44 Commits

Author SHA1 Message Date
39781d0149 fix(AI plugin): bufferize response with file
Bufferizing response with a file allows to avoid crash caused by data transfer, based on plugins API issue in the CProof.

Minor changes: change default model and set correct xai models in the docs
2025-09-08 13:05:52 +02:00
94e9b155ef feat: Add AI chat plugin, ai.py
Add ai.py plugin to support AI model interaction via LiteLLM. Requires litellm.
2025-07-15 15:16:12 +02:00
38b8a2fdf6 Include external plugins, minor cleanup
- Consolidate plugins from other repositories to make their installation easier
- Make minor changes to improve formatting
2025-07-14 12:14:25 +02:00
325316c3b2 ref: Cleanup and rearrange files, update documentation
- Remove Ruby files since they aren't supported anymore
- Remove test and samples files
- Move all the "stable" plugins to `src` folder to follow standard conventions
- Update links and descriptions in the documentation
2025-07-14 12:11:06 +02:00
68800cf823 feat(sounds): add per-JID mute support and enhance playback behavior
Some users requested the ability to mute notifications from specific JIDs
without disabling all sounds. This change introduces `mute` and `unmute`
commands to allow fine-grained control over sound playback.

While implementing this, the sound path expansion logic was improved by
switching to `pathlib` with `lru_cache` for efficiency and clarity.
Playback is now also rate-limited to avoid overlapping audio triggers
from rapid message bursts.

This lays the groundwork for more user-specific customization of sound
notifications while keeping the experience smooth and reliable.

Additionally the bug with ignoring sound's `kind` was fixed: now the plugin respects specific sounds set for rooms, chats and private messages within rooms.

Finally, code was formatted via Black to avoid ambiguity in the formatting.
2025-07-06 02:16:39 +02:00
Michael Vetter
179785ce4e Merge pull request #30 from subfxnet/jsh/variable-audioplayer
Add several new commands to the sounds.py plugin:
2024-04-23 21:35:39 +02:00
Jason Hensley
ade6439157 Add several new commands to the sounds.py plugin:
- /sounds set audioplayer <cmd>

  Allows configuring any CLI tool to be used for audio playback.  This
opens up the ability to play media types beyond mpeg3.

- /sounds reset audioplayer

  Resets the audio player to the default of "mpg123 -q".

- /sounds test chat|room|private

  Tests the /sounds setup to ensure the expected sounds will play on the
proper events.
2024-04-23 10:58:23 -04:00
Michael Vetter
0812b6c1e8 Merge pull request #29 from ReimarPB/reimar-plugins
Add Reimar's plugin collection to README
2023-12-10 17:34:09 +01:00
Reimar
92e708a10b Add external plugin collection from Reimar 2023-12-10 17:01:15 +01:00
Michael Vetter
255846954a Merge pull request #28 from H3rnand3zzz/patch-2
Refactor emoticons.py plugin, add new emoticons, and remove dependency
2023-11-10 22:16:06 +01:00
John Hernandez
339ad336e2 Refactor emoticons.py plugin, add new emoticons, and remove dependency
Enhance the emoticons.py plugin by using a dictionary for emoticon
replacement. Add new emoticons to the dictionary and remove the
dependency on the 'future' module. Update documentation accordingly.
2023-11-09 10:02:33 +01:00
Michael Vetter
2bff450e80 Merge pull request #27 from H3rnand3zzz/patch-1
Fix TypeError in syscmd.py plugin
2023-10-23 12:11:17 +02:00
John Hernandez
e53ecc76da Fix TypeError in syscmd.py plugin
Fix `TypeError: can only concatenate str (not "bytes") to str`
2023-10-23 12:02:17 +02:00
Michael Vetter
6559d236d1 Merge pull request #26 from H3rnand3zzz/patch-1
Add reference to profanity plugins from H3rnand3zzz
2023-04-14 20:02:38 +02:00
John Hernandez
f8587a26ca Add reference to profanity plugins by H3rnand3zzz 2023-04-14 19:47:09 +02:00
Michael Vetter
65a820b27a Add moj 2022-03-06 20:24:41 +01:00
Michael Vetter
c3a9420ad7 Add more external plugins 2022-02-18 11:21:18 +01:00
Michael Vetter
0ef7d9d1de Add profanity-notifycmd link 2022-02-18 11:11:31 +01:00
Michael Vetter
eb87f21922 Update master description 2021-09-21 14:59:05 +02:00
Michael Vetter
3972260733 Merge pull request #24 from diego-treitos/master
Added HTML strip plugin
2021-07-02 20:47:13 +02:00
Diego Blanco
efa13667ca Added HTML strip plugin 2021-07-02 20:38:48 +02:00
Michael Vetter
f5956dddf0 Merge pull request #22 from wasamasa/master
Use shlex to shell quote user input properly
2021-01-28 11:12:19 +01:00
Vasilij Schneidermann
ab9a606473 Use shlex to shell quote user input properly 2021-01-27 21:53:28 +01:00
Michael Vetter
6567aae509 Merge pull request #20 from lordwolfchild/master
Add autocorrector.py
2020-05-21 09:21:24 +02:00
Thomas Thiel
a479d2da7d added autocorrector.py
An autocorrection plugin for the messy typist.

replaces typos you make from a list of strings. can be used for other
purposes, too, i think, but I use it to correct my messy typing.
2020-05-20 17:34:58 +02:00
Stefan
2d9793cf32 Microblog (#18)
* Microblog Plugin

Display information about new blog entry
2020-03-02 09:12:13 +01:00
Michael Vetter
55da731bf6 Merge pull request #19 from DebXWoody/sample02
Plugin Example 02 - Define an own command
2020-03-02 09:11:01 +01:00
DebXWoody
fdb6d70f63 Plugin Example 02 - Define an own command 2020-02-29 07:14:52 +01:00
Stefan
19ff42c94f Profanity C-Plugin Example 01 (#16)
Profanity C-Plugin Example

This example provides a small example how to define and compile the callbacks.
2020-02-27 15:06:10 +01:00
Michael Vetter
8a3089b5be Merge pull request #14 from mdosch/patch-1
Update project URL
2019-08-07 14:33:48 +02:00
Martin
d69df863fe Update project URL
http://profanity.im -> https://profanity-im.github.io
2019-08-07 13:48:56 +02:00
James Booth
c49d29846c Update README.md 2017-01-29 00:27:02 +00:00
James Booth
16cb1452c0 Update test plugins 2017-01-28 19:45:42 +00:00
James Booth
4f9f513c08 Add pre chat a room message blocking 2017-01-22 16:00:58 +00:00
James Booth
9fb6ea1cf3 Added tests for encryption plugin changes 2017-01-22 00:57:09 +00:00
James Booth
c0eefa2ff7 syscmd.py: Make newlines configurable
issue #9
2016-11-13 21:50:45 +00:00
James Booth
e84dd620c8 Update README 2016-11-13 17:27:57 +00:00
James Booth
ce1c777338 Update README 2016-11-13 17:25:41 +00:00
James Booth
148c4b3ee1 Restructure project 2016-11-13 17:01:39 +00:00
James Booth
3f6ac94acb paste.py: newline option only for multiline text
issue #5
2016-11-13 16:14:41 +00:00
James Booth
3bbe6c6202 paste,py: add newline option
issue #5
2016-11-13 15:36:54 +00:00
James Booth
ff4b5198d8 sounds.py: don't play sounds on messages from self 2016-11-06 23:12:48 +00:00
James Booth
e2ab722f93 Add filepath autocompletion to sounds.py 2016-10-10 23:32:04 +01:00
James Booth
3f11319a5b Update plugins to use filepath completion 2016-10-10 22:46:53 +01:00
52 changed files with 6079 additions and 3305 deletions

View File

@@ -1,16 +0,0 @@
module ChatStart
@@starts = {
"prof1@panesar" => [ "\"Prof 2\"", "prof3@panesar" ],
"prof2@panesar" => [ "prof1@panesar" ]
}
def self.prof_on_connect(account_name, fulljid)
if @@starts[account_name]
@@starts[account_name].each { | contact |
Prof::send_line("/msg " + contact)
}
Prof::send_line("/win 1")
end
end
end

138
README.md
View File

@@ -1,126 +1,56 @@
profanity-plugins
=================
# CProof Plugins
Plugin support for Profanity is currently in development.
[Website documentation](https://jabber.space/guides/plugins/)
* The `master` branch of Profanity now includes support for C and Python plugins.
* The `plugins` branch contains additional support for Ruby and Lua, but is unstable.
`Master` is a development branch compatible with the latest version of CProof.
For a list of outstanding issues before releasing 0.5.0, see: https://github.com/boothj5/profanity/milestones/0.5.0
## Installing plugins
* [Website documentation](http://www.profanity.im/plugins.html)
Use the `/plugins install` command, e.g.
Building Profanity with plugin support
--------------------------------------
```bash
/plugins install ~/path-to/sounds.py
```
Additional dependencies required:
Alternatively, you can use URLs to raw files to install Python plugins. For example,
```bash
/plugins install https://git.jabber.space/devs/cproof-plugins/raw/branch/master/src/sounds.py
```
See the `/help plugins` command for further plugin management options.
## Plugins description
To be described.
## Building CProof with plugin support
Dependencies required:
```
autoconf-archive
libtool
```
Plus the development packages for supported languages:
For Python plugins the following is also required:
```
python-dev
lua-dev
ruby-dev
```
Build with:
## Developing plugins
```
./bootstrap.sh
./configure
make
sudo make install
```
API Documentation is available on our website, [jabber.space](https://jabber.space/).
- [Python API](https://jabber.space/api-reference/python/prof/)
- [Python hooks](https://jabber.space/api-reference/python/plugin/)
- [C API](https://jabber.space/api-reference/c/profapi/)
- [C hooks](https://jabber.space/api-reference/c/profhooks/)
After building, run `profanity -v` to see which language support is available, example output:
## External plugins (C)
```
Profanity, version 0.5.0dev.plugins-python.63a7316
Copyright (C) 2012 - 2016 James Booth <boothj5web@gmail.com>.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Build information:
XMPP library: libmesode
Desktop notification support: Enabled
OTR support: Enabled
PGP support: Enabled
C plugins: Enabled
Python plugins: Enabled
```
Installing plugins
------------------
Use the `/plugins install` command, e.g.
```
/plugins install ~/projects-git/profanity-plugins/say.py
```
See the `/help plugins` command for further plugin management options.
More help on plugins
--------------------
* `/plugins` - Shows a list of loaded plugins.
* `/help commands plugins` - Shows commands defined by plugins
* `/help <plugin_cmd>` - Show help for a plugin command, e.g. `/help browser`
Plugin themes
-------------
The plugins API includes functions to print themed output to the console or a plugins window. The themes need to be added to
```
~/.local/share/profanity/plugin_themes
```
For example ([syscmd.py](https://github.com/boothj5/profanity-plugins/blob/master/syscmd.py) plugin):
```
[system]
command=cyan
result=green
```
Plugin settings
---------------
The plugins API also includes functions to read and write settings. The settings are stored in:
```
~/.local/share/profanity/plugin_settings
```
For example ([say.py](https://github.com/boothj5/profanity-plugins/blob/master/say.py) plugin):
```
[say]
args=-v english -s 120
enabled=active
```
Developing plugins
------------------
API Documentation:
* [Python API](http://www.profanity.im/plugins/python/html/prof.html)
* [Python hooks](http://www.profanity.im/plugins/python/html/plugin.html)
* [C API](http://www.profanity.im/plugins/c/html/profapi_8h.html)
* [C hooks](http://www.profanity.im/plugins/c/html/profhooks_8h.html)
Example test plugins (Ruby and Lua examples might not be up to date):
* [tests/test-c-plugin.c](https://github.com/boothj5/profanity-plugins/blob/master/tests/test-c-plugin/test-c-plugin.c)
* [tests/python-test.py](https://github.com/boothj5/profanity-plugins/blob/master/tests/python-test.py)
* [tests/RubyTest.rb](https://github.com/boothj5/profanity-plugins/blob/master/tests/RubyTest.rb)
* [tests/luatest.lua](https://github.com/boothj5/profanity-plugins/blob/master/tests/luatest.lua)
- [profanity-plugins by Reimar](https://github.com/ReimarPB/profanity-plugins) - Collection of plugins
## External plugins (Python)
- [profanity-copy-message-plugin](https://github.com/lonski/profanity-copy-message-plugin) - Copy messages to clipboard.

View File

@@ -1,51 +0,0 @@
import prof
from chatterbotapi import ChatterBotFactory, ChatterBotType
factory = ChatterBotFactory()
bot = factory.create(ChatterBotType.CLEVERBOT)
# bot = factory.create(ChatterBotType.JABBERWACKY)
# bot = factory.create(ChatterBotType.PANDORABOTS, 'b0dafd24ee35a477')
bot_session = {}
bot_state = False
def prof_post_chat_message_display(barejid, resource, message):
if bot_state:
if barejid not in bot_session:
bot_session[barejid] = bot.create_session()
response = bot_session[barejid].think(message)
prof.send_line("/msg " + barejid + " " + response)
def _cmd_chatterbot(state):
global bot_state
if state == "enable":
prof.cons_show("ChatterBot Activated")
bot_state = True
elif state == "disable":
prof.cons_show("ChatterBot Stopped")
bot_state = False
else:
if bot_state:
prof.cons_show("ChatterBot is running - current sessions:")
prof.cons_show(str(bot_session))
else:
prof.cons_show("ChatterBot is stopped - /chatterbot enable to activate.")
def prof_init(version, status, account_name, fulljid):
synopsis = [
"/chatterbot",
"/chatterbot enable|disable"
]
description = "ChatterBot, running with no args will show the current chatterbot status"
args = [
[ "enable", "Enable chatterbot" ],
[ "disable", "Disable chatterbot" ]
]
examples = []
prof.register_command("/chatterbot", 0, 1, synopsis, description, args, examples, _cmd_chatterbot)
prof.completer_add("/chatterbot", [ "enable", "disable" ])

View File

@@ -1,190 +0,0 @@
import re
import sys
import hashlib
if sys.version_info >= (3, 0):
from urllib.request import build_opener, HTTPCookieProcessor, urlopen
from urllib.parse import urlencode
import http.cookiejar as cookielib
else:
from urllib import urlencode, urlopen
from urllib2 import build_opener, HTTPCookieProcessor
import cookielib
import uuid
import xml.dom.minidom
"""
chatterbotapi
Copyright (C) 2011 pierredavidbelanger@gmail.com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
#################################################
# API
#################################################
class ChatterBotType:
CLEVERBOT = 1
JABBERWACKY = 2
PANDORABOTS = 3
class ChatterBotFactory:
def create(self, type, arg = None):
if type == ChatterBotType.CLEVERBOT:
return _Cleverbot('http://www.cleverbot.com', 'http://www.cleverbot.com/webservicemin', 35)
elif type == ChatterBotType.JABBERWACKY:
return _Cleverbot('http://jabberwacky.com', 'http://jabberwacky.com/webservicemin', 29)
elif type == ChatterBotType.PANDORABOTS:
if arg == None:
raise Exception('PANDORABOTS needs a botid arg')
return _Pandorabots(arg)
return None
class ChatterBot:
def create_session(self):
return None
class ChatterBotSession:
def think_thought(self, thought):
return thought
def think(self, text):
thought = ChatterBotThought()
thought.text = text
return self.think_thought(thought).text
class ChatterBotThought:
pass
#################################################
# Cleverbot impl
#################################################
class _Cleverbot(ChatterBot):
def __init__(self, baseUrl, serviceUrl, endIndex):
self.baseUrl = baseUrl
self.serviceUrl = serviceUrl
self.endIndex = endIndex
def create_session(self):
return _CleverbotSession(self)
class _CleverbotSession(ChatterBotSession):
def __init__(self, bot):
self.bot = bot
self.vars = {}
self.vars['start'] = 'y'
self.vars['icognoid'] = 'wsf'
self.vars['fno'] = '0'
self.vars['sub'] = 'Say'
self.vars['islearning'] = '1'
self.vars['cleanslate'] = 'false'
self.cookieJar = cookielib.CookieJar()
self.opener = build_opener(HTTPCookieProcessor(self.cookieJar))
self.opener.open(self.bot.baseUrl)
def think_thought(self, thought):
self.vars['stimulus'] = thought.text
data = urlencode(self.vars)
data_to_digest = data[9:self.bot.endIndex]
data_digest = hashlib.md5(data_to_digest.encode('utf-8')).hexdigest()
data = data + '&icognocheck=' + data_digest
url_response = self.opener.open(self.bot.serviceUrl, data.encode('utf-8'))
response = str(url_response.read())
response_values = re.split(r'\\r|\r', response)
#self.vars['??'] = _utils_string_at_index(response_values, 0)
self.vars['sessionid'] = _utils_string_at_index(response_values, 1)
self.vars['logurl'] = _utils_string_at_index(response_values, 2)
self.vars['vText8'] = _utils_string_at_index(response_values, 3)
self.vars['vText7'] = _utils_string_at_index(response_values, 4)
self.vars['vText6'] = _utils_string_at_index(response_values, 5)
self.vars['vText5'] = _utils_string_at_index(response_values, 6)
self.vars['vText4'] = _utils_string_at_index(response_values, 7)
self.vars['vText3'] = _utils_string_at_index(response_values, 8)
self.vars['vText2'] = _utils_string_at_index(response_values, 9)
self.vars['prevref'] = _utils_string_at_index(response_values, 10)
#self.vars['??'] = _utils_string_at_index(response_values, 11)
self.vars['emotionalhistory'] = _utils_string_at_index(response_values, 12)
self.vars['ttsLocMP3'] = _utils_string_at_index(response_values, 13)
self.vars['ttsLocTXT'] = _utils_string_at_index(response_values, 14)
self.vars['ttsLocTXT3'] = _utils_string_at_index(response_values, 15)
self.vars['ttsText'] = _utils_string_at_index(response_values, 16)
self.vars['lineRef'] = _utils_string_at_index(response_values, 17)
self.vars['lineURL'] = _utils_string_at_index(response_values, 18)
self.vars['linePOST'] = _utils_string_at_index(response_values, 19)
self.vars['lineChoices'] = _utils_string_at_index(response_values, 20)
self.vars['lineChoicesAbbrev'] = _utils_string_at_index(response_values, 21)
self.vars['typingData'] = _utils_string_at_index(response_values, 22)
self.vars['divert'] = _utils_string_at_index(response_values, 23)
response_thought = ChatterBotThought()
response_thought.text = _utils_string_at_index(response_values, 16)
return response_thought
#################################################
# Pandorabots impl
#################################################
class _Pandorabots(ChatterBot):
def __init__(self, botid):
self.botid = botid
def create_session(self):
return _PandorabotsSession(self)
class _PandorabotsSession(ChatterBotSession):
def __init__(self, bot):
self.vars = {}
self.vars['botid'] = bot.botid
self.vars['custid'] = uuid.uuid1()
def think_thought(self, thought):
self.vars['input'] = thought.text
data = urlencode(self.vars)
url_response = urlopen('http://www.pandorabots.com/pandora/talk-xml', data)
response = url_response.read()
response_dom = xml.dom.minidom.parseString(response)
response_thought = ChatterBotThought()
that_elements = response_dom.getElementsByTagName('that')
if that_elements is None or len(that_elements) == 0 or that_elements[0] is None:
return ''
that_elements_child_nodes = that_elements[0].childNodes
if that_elements_child_nodes is None or len(that_elements_child_nodes) == 0 or that_elements_child_nodes[0] is None:
return ''
that_elements_child_nodes_data = that_elements_child_nodes[0].data
if that_elements_child_nodes_data is None:
return ''
response_thought.text = that_elements_child_nodes_data.strip()
return response_thought
#################################################
# Utils
#################################################
def _utils_string_at_index(strings, index):
if len(strings) > index:
return strings[index]
else:
return ''

View File

@@ -1,27 +0,0 @@
rm -rf ~/.local/share/profanity/plugins/*
cp ascii.py ~/.local/share/profanity/plugins/.
cp syscmd.py ~/.local/share/profanity/plugins/.
cp paste.py ~/.local/share/profanity/plugins/.
cp browser.py ~/.local/share/profanity/plugins/.
cp platform-info.py ~/.local/share/profanity/plugins/.
cp say.py ~/.local/share/profanity/plugins/.
cp whoami.py ~/.local/share/profanity/plugins/.
cp wikipedia-prof.py ~/.local/share/profanity/plugins/.
cp clients.py ~/.local/share/profanity/plugins/.
cp presence_notify.py ~/.local/share/profanity/plugins/.
cp tests/python-test.py ~/.local/share/profanity/plugins/.
cd pid
make clean
make
cp pid.so ~/.local/share/profanity/plugins/.
cd ..
cd tests/test-c-plugin
make clean
make
cp test-c-plugin.so ~/.local/share/profanity/plugins/.
cd ../..

View File

@@ -1,30 +0,0 @@
"""
Convert smileys ( :) :( etc) to the Unicode character representation
Dependencies:
pip install future
"""
import prof
from builtins import chr
def _emote(input_str):
result = input_str
result = result.replace(":-)", chr(9786))
result = result.replace(":)", chr(9786))
result = result.replace(":-(", chr(9785))
result = result.replace(":(", chr(9785))
return result
def prof_pre_chat_message_display(barejid, resource, message):
return _emote(message)
def prof_pre_room_message_display(barejid, nick, message):
return _emote(message)
def prof_pre_priv_message_display(barejid, nick, message):
return _emote(message)

View File

@@ -1,531 +0,0 @@
"""
Manage jenkins jobs
Prerequisites:
The jenkinsapi module is required https://pypi.python.org/pypi/jenkinsapi
To install the module using pip:
sudo pip install jenkinsapi
The plugin polls the jenkins server at 'jenkins_url' every 'jenkins_poll_interval' seconds.
The results are checked by profanity every 'prof_cb_interval'.
New failures are reported in the jenkins window, and a desktop notification is sent.
The 'prof_remind_interval' specifies the time between reminder notifications of broken builds
"""
import prof
import os
import threading
import time
import webbrowser
import urllib2
import base64
import jenkinsapi
from jenkinsapi.jenkins import Jenkins
# Settings
jenkins_url = "http://localhost:8080"
username = None
password = None
jenkins_poll_interval = 5
prof_cb_interval = 5
prof_remind_interval = 10
enable_notify = True
enable_remind = True
# Global state
last_state = {}
STATE_SUCCESS = "SUCCESS"
STATE_UNSTABLE = "UNSTABLE"
STATE_FAILURE = "FAILURE"
STATE_QUEUED = "QUEUED"
STATE_RUNNING = "RUNNING"
STATE_NOBUILDS = "NOBUILDS"
STATE_UNKNOWN = "UNKNOWN"
win_tag = "Jenkins"
poll_fail = False
poll_fail_message = None
job_list = None
changes_list = None
def _help():
prof.win_show(win_tag, "Commands:")
prof.win_show(win_tag, " /jenkins help - Show this help")
prof.win_show(win_tag, " /jenkins jobs - List all jobs")
prof.win_show(win_tag, " /jenkins failing - List all failing jobs")
prof.win_show(win_tag, " /jenkins passing - List all passing jobs")
prof.win_show(win_tag, " /jenkins unstable - List all unstable jobs")
prof.win_show(win_tag, " /jenkins build [job] - Trigger build for job")
prof.win_show(win_tag, " /jenkins open [job] - Open job in browser")
prof.win_show(win_tag, " /jenkins log [job] - Show the latest build log")
prof.win_show(win_tag, " /jenkins remind on|off - Enable/disable reminder notifications")
prof.win_show(win_tag, " /jenkins notify on|off - Enable/disable build notifications")
prof.win_show(win_tag, " /jenkins settings - Show current settings")
def _settings():
prof.win_show(win_tag, "Jenkins settings:")
prof.win_show(win_tag, " Jenkins URL : " + jenkins_url)
prof.win_show(win_tag, " Jenkins poll interval : " + str(jenkins_poll_interval) + " seconds")
prof.win_show(win_tag, " Profanity update interval : " + str(prof_cb_interval) + " seconds")
prof.win_show(win_tag, " Reminder interval : " + str(prof_remind_interval) + " seconds")
prof.win_show(win_tag, " Notifications enabled : " + str(enable_notify))
prof.win_show(win_tag, " Reminders enabled : " + str(enable_remind))
def _safe_remove(jobname, state):
if jobname in last_state[state]:
last_state[state].remove(jobname)
def _set_state(jobname, state):
if not jobname in last_state[state]:
_safe_remove(jobname, STATE_SUCCESS)
_safe_remove(jobname, STATE_UNSTABLE)
_safe_remove(jobname, STATE_FAILURE)
_safe_remove(jobname, STATE_QUEUED)
_safe_remove(jobname, STATE_RUNNING)
_safe_remove(jobname, STATE_NOBUILDS)
_safe_remove(jobname, STATE_UNKNOWN)
last_state[state].append(jobname)
return True
else:
return False
def _open_job_url(url):
savout = os.dup(1)
saverr = os.dup(2)
os.close(1)
os.close(2)
os.open(os.devnull, os.O_RDWR)
try:
webbrowser.open(url, new=2)
finally:
os.dup2(savout, 1)
os.dup2(saverr, 2)
class JobList():
def __init__(self):
self.jobs = []
def add_job(self, name, build_number, state):
self.jobs.append((name, build_number, state))
def get_jobs(self, in_state=None):
if not in_state:
return self.jobs
else:
result = []
for name, build_number, state in self.jobs:
if state == in_state:
result.append((name, build_number, state))
return result
def get_job(self, jobname):
for name, build_number, state in self.jobs:
if name == jobname:
return name, build_number
return None
def contains_job(self, jobname):
for name, build_number, state in self.jobs:
if name == jobname:
return True
return False
def num_in_state(self, given_state):
count = 0
for name, build_number, state in self.jobs:
if state == given_state:
count = count + 1
return count
class JobUpdates():
def __init__(self):
self.states = {}
self.states[STATE_FAILURE] = []
self.states[STATE_SUCCESS] = []
self.states[STATE_UNSTABLE] = []
self.states[STATE_QUEUED] = []
self.states[STATE_RUNNING] = []
def add_update(self, state, name, build_number=None):
if build_number:
self.states[state].append((name, build_number))
else:
self.states[state].append(name)
def get_in_state(self, state):
return self.states[state]
def _process_build(name, build, new_job_list, new_changes_list):
if build.get_status() == STATE_FAILURE:
new_job_list.add_job(name, build.get_number(), STATE_FAILURE)
changed = _set_state(name, STATE_FAILURE)
if changed:
new_changes_list.add_update(STATE_FAILURE, name, build.get_number())
elif build.get_status() == STATE_SUCCESS:
new_job_list.add_job(name, build.get_number(), STATE_SUCCESS)
changed = _set_state(name, STATE_SUCCESS)
if changed:
new_changes_list.add_update(STATE_SUCCESS, name, build.get_number())
elif build.get_status() == STATE_UNSTABLE:
new_job_list.add_job(name, build.get_number(), STATE_UNSTABLE)
changed = _set_state(name, STATE_UNSTABLE)
if changed:
new_changes_list.add_update(STATE_UNSTABLE, name, build.get_number())
def _process_queued_or_running(name, job, new_job_list, new_changes_list):
if job.is_queued():
new_job_list.add_job(name, None, STATE_QUEUED)
changed = _set_state(name, STATE_QUEUED)
if changed:
new_changes_list.add_update(STATE_QUEUED, name)
elif job.is_running():
new_job_list.add_job(name, None, STATE_RUNNING)
changed = _set_state(name, STATE_RUNNING)
if changed:
new_changes_list.add_update(STATE_RUNNING, name)
def _jenkins_poll():
global poll_fail
global poll_fail_message
global job_list
global changes_list
job_ac = []
while True:
time.sleep(jenkins_poll_interval)
try:
j = Jenkins(jenkins_url, username, password)
except Exception, e:
poll_fail = True
poll_fail_message = str(e)
else:
poll_fail = False
poll_fail_message = None
new_job_list = JobList()
new_changes_list = JobUpdates()
for name, job in j.get_jobs():
if not job.is_queued_or_running():
build = job.get_last_build_or_none()
if build:
_process_build(name, build, new_job_list, new_changes_list)
else:
new_job_list.add_job(name, None, STATE_NOBUILDS)
else:
_process_queued_or_running(name, job, new_job_list, new_changes_list)
if not job_list:
job_ac.append(name)
if not job_list:
prof.completer_add("/jenkins build", job_ac)
prof.completer_add("/jenkins open", job_ac)
prof.completer_add("/jenkins log", job_ac)
job_list = new_job_list
changes_list = new_changes_list
def _prof_callback():
global changes_list
if poll_fail:
if not prof.win_exists(win_tag):
prof.win_create(win_tag, _handle_input)
prof.win_show(win_tag, "Could not connect to jenkins, see the logs.")
if poll_fail_message:
prof.log_warning("Jenkins poll failed: " + str(poll_fail_message))
else:
prof.log_warning("Jenkins poll failed")
elif changes_list:
for name in changes_list.get_in_state(STATE_QUEUED):
if not prof.win_exists(win_tag):
prof.win_create(win_tag, _handle_input)
prof.win_show_themed(win_tag, None, None, "cyan", name + " " + STATE_QUEUED)
for name in changes_list.get_in_state(STATE_RUNNING):
if not prof.win_exists(win_tag):
prof.win_create(win_tag, _handle_input)
prof.win_show_themed(win_tag, None, None, "cyan", name + " " + STATE_RUNNING)
for name, build_number in changes_list.get_in_state(STATE_SUCCESS):
if not prof.win_exists(win_tag):
prof.win_create(win_tag, _handle_input)
prof.win_show_themed(win_tag, None, None, "green", name + " #" + str(build_number) + " " + STATE_SUCCESS)
if enable_notify:
prof.notify(name + " " + STATE_SUCCESS, 5000, "Jenkins")
for name, build_number in changes_list.get_in_state(STATE_UNSTABLE):
if not prof.win_exists(win_tag):
prof.win_create(win_tag, _handle_input)
prof.win_show_themed(win_tag, None, None, "yellow", name + " #" + str(build_number) + " " + STATE_UNSTABLE)
if enable_notify:
prof.notify(name + " " + STATE_UNSTABLE, 5000, "Jenkins")
for name, build_number in changes_list.get_in_state(STATE_FAILURE):
if not prof.win_exists(win_tag):
prof.win_create(win_tag, _handle_input)
prof.win_show_themed(win_tag, None, None, "red", name + " #" + str(build_number) + " " + STATE_FAILURE)
if enable_notify:
prof.notify(name + " " + STATE_FAILURE, 5000, "Jenkins")
changes_list = None
def _handle_input(win, line):
pass
def _list_jobs(jobs):
for name, build_number, state in jobs:
if state == STATE_SUCCESS:
prof.win_show_themed(win_tag, None, None, "green", " " + name + " #" + str(build_number) + " " + STATE_SUCCESS)
elif state == STATE_UNSTABLE:
prof.win_show_themed(win_tag, None, None, "yellow", " " + name + " #" + str(build_number) + " " + STATE_UNSTABLE)
elif state == STATE_FAILURE:
prof.win_show_themed(win_tag, None, None, "red", " " + name + " #" + str(build_number) + " " + STATE_FAILURE)
elif state == STATE_NOBUILDS:
prof.win_show(win_tag, " " + name + ", no builds")
else:
prof.win_show_themed(win_tag, None, None, "cyan", " " + name + " " + state)
def _build_job(job):
try:
request = urllib2.Request(jenkins_url + "/job/" + job + "/build");
if username:
basicauth = base64.encodestring("%s:%s" %(username, password)).replace("\n", "")
request.add_header("Authorization", "Basic %s" %basicauth);
request.add_data("")
urllib2.urlopen(request)
except Exception, e:
prof.win_show(win_tag, "Failed to build " + job + ", see the logs.")
prof.log_warning("Failed to build " + job + ": " + str(e))
else:
prof.win_show(win_tag, "Build request sent for " + job)
def _job_log(job):
name = job[0]
build_no = job[1]
if build_no:
try:
response = urllib2.urlopen(jenkins_url + "/job/" + name + "/" + str(build_no) + "/consoleText")
except Exception, e:
prof.win_show(win_tag, "Unable to fetch log for " + name + "#" + str(build_no) + ", see the logs.")
prof.log_warning("Unable to fetch log for " + name + "#" + str(build_no) + ": " + str(e))
else:
log_str = "Log for " + name + " #" + str(build_no) + ":\n" + response.read()
prof.win_show(win_tag, log_str)
else:
prof.win_show(win_tag, "No build found for " + name)
def _cmd_jenkins(cmd=None, arg=None):
global enable_remind
global enable_notify
if not prof.win_exists(win_tag):
prof.win_create(win_tag, _handle_input)
prof.win_focus(win_tag)
if cmd == "jobs":
if job_list and job_list.get_jobs():
prof.win_show(win_tag, "Jobs:")
_list_jobs(job_list.get_jobs())
else:
prof.win_show(win_tag, "No job data yet.")
elif cmd == "failing":
if job_list:
jobs_in_state = job_list.get_jobs(STATE_FAILURE)
if jobs_in_state:
prof.win_show(win_tag, "Failing jobs:")
_list_jobs(jobs_in_state)
else:
prof.win_show(win_tag, "No failing jobs.")
else:
prof.win_show(win_tag, "No job data yet.")
elif cmd == "passing":
if job_list:
jobs_in_state = job_list.get_jobs(STATE_SUCCESS)
if jobs_in_state:
prof.win_show(win_tag, "Passing jobs:")
_list_jobs(jobs_in_state)
else:
prof.win_show(win_tag, "No passing jobs.")
else:
prof.win_show(win_tag, "No job data yet.")
elif cmd == "unstable":
if job_list:
jobs_in_state = job_list.get_jobs(STATE_UNSTABLE)
if jobs_in_state:
prof.win_show(win_tag, "Unstable jobs:")
_list_jobs(jobs_in_state)
else:
prof.win_show(win_tag, "No unstable jobs.")
else:
prof.win_show(win_tag, "No job data yet.")
elif cmd == "build":
if not arg:
prof.win_show(win_tag, "You must supply a job argument.")
elif job_list and job_list.contains_job(arg):
_build_job(arg)
else:
prof.win_show(win_tag, "No such job: " + arg)
elif cmd == "open":
if not arg:
prof.win_show(win_tag, "You must supply a job argument.")
elif job_list and job_list.contains_job(arg):
_open_job_url(jenkins_url + "/job/" + arg)
else:
prof.win_show(win_tag, "No such job: " + arg)
elif cmd == "remind":
if not arg:
prof.win_show(win_tag, "You must specify either 'on' or 'off'.")
elif arg == "on":
enable_remind = True
prof.win_show(win_tag, "Reminder notifications enabled.")
elif arg == "off":
enable_remind = False
prof.win_show(win_tag, "Reminder notifications disabled.")
else:
prof.win_show(win_tag, "You must specify either 'on' or 'off'.")
elif cmd == "notify":
if not arg:
prof.win_show(win_tag, "You must specify either 'on' or 'off'.")
elif arg == "on":
enable_notify = True
prof.win_show(win_tag, "Build notifications enabled.")
elif arg == "off":
enable_notify = False
prof.win_show(win_tag, "Build notifications disabled.")
else:
prof.win_show(win_tag, "You must specify either 'on' or 'off'.")
elif cmd == "settings":
_settings()
elif cmd == "help":
_help()
elif cmd == "log":
if not arg:
prof.win_show(win_tag, "You must specify a job.")
else:
job = job_list.get_job(arg)
if job:
_job_log(job)
else:
prof.win_show(win_tag, "No job found: " + arg)
else:
prof.win_show(win_tag, "Unknown command.")
def _remind():
if enable_remind and job_list:
notify_string = ""
failing = job_list.num_in_state(STATE_FAILURE)
unstable = job_list.num_in_state(STATE_UNSTABLE)
if failing == 1:
notify_string = notify_string + "1 failing build"
if failing > 1:
notify_string = notify_string + str(failing) + " failing builds"
if failing > 0 and unstable > 0:
notify_string = notify_string + "\n"
if unstable == 1:
notify_string = notify_string + "1 unstable build"
if unstable > 1:
notify_string = notify_string + str(unstable) + " unstable builds"
if not notify_string == "":
prof.notify(notify_string, 5000, "Jenkins")
def prof_init(version, status, account_name, fulljid):
last_state[STATE_SUCCESS] = []
last_state[STATE_UNSTABLE] = []
last_state[STATE_FAILURE] = []
last_state[STATE_QUEUED] = []
last_state[STATE_RUNNING] = []
last_state[STATE_NOBUILDS] = []
last_state[STATE_UNKNOWN] = []
jenkins_t = threading.Thread(target=_jenkins_poll)
jenkins_t.daemon = True;
jenkins_t.start()
prof.register_timed(_prof_callback, prof_cb_interval)
prof.register_timed(_remind, prof_remind_interval)
prof.completer_add(
"/jenkins", [
"help",
"jobs",
"failing",
"passing",
"unstable",
"build",
"open",
"log",
"remind",
"notify",
"settings"
]
);
prof.completer_add(
"/jenkins remind", [
"on",
"off"
]
);
prof.completer_add(
"/jenkins notify", [
"on",
"off"
]
);
synopsis = [
"/jenkins jobs|failing|passing|unstable",
"/jenkins build <job>",
"/jenkins open <job>",
"/jenkins log <job>",
"/jenkins remind on|off",
"/jenkins notify on|off",
"/jenkins settings"
]
description = "Monitor, run and view Jenkins jobs."
args = [
[ "jobs", "List all jobs" ],
[ "failing", "List all failing jobs" ],
[ "passing", "List all passing jobs" ],
[ "unstable", "List all unstable jobs" ],
[ "build <job>", "Trigger build for job" ],
[ "open <job>", "Open job in browser" ],
[ "log <job>", "Show the latest build log for job" ],
[ "remind on|off", "Enable/disable reminder notifications" ],
[ "notify on|off", "Enable/disable build notifications" ],
[ "settings", "Show current settings" ]
]
examples = [
"/kenkins failing",
"/kenkins build stabber-build",
"/kenkins remind off"
]
prof.register_command("/jenkins", 0, 2, synopsis, description, args, examples, _cmd_jenkins)
def prof_on_start():
prof.win_create(win_tag, _handle_input)
prof.win_show(win_tag, "Jenkins plugin started.")

View File

@@ -1,27 +0,0 @@
"""
Paste the contents of the clipboard when in a chat or room window.
Dependencies:
pip install future
sudo apt-get python-tk (or python3-tk)
"""
import prof
import tkinter as tk
def _cmd_paste():
root = tk.Tk(baseName="")
root.withdraw()
result = root.clipboard_get()
prof.send_line(u'\u000A' + result)
def prof_init(version, status, account_name, fulljid):
synopsis = [
"/paste"
]
description = "Paste contents of clipboard."
args = []
examples = []
prof.register_command("/paste", 0, 0, synopsis, description, args, examples, _cmd_paste)

198
sounds.py
View File

@@ -1,198 +0,0 @@
"""
Play sounds on various events.
Requires mpg123 to be installed on the host system:
sudo apt-get install mpg123
brew install mpg123
"""
import subprocess
from os.path import expanduser
import prof
def _play_sound(soundfile):
if soundfile.startswith("~/"):
soundfilenew = soundfile.replace("~", expanduser("~"), 1)
else:
soundfilenew = soundfile
subprocess.Popen(["mpg123", "-q", soundfilenew])
def _cmd_sounds(arg1=None, arg2=None, arg3=None):
if not arg1:
prof.cons_show("")
enabled = prof.settings_boolean_get("sounds", "enabled", False)
chatsound = prof.settings_string_get("sounds", "chat", None)
roomsound = prof.settings_string_get("sounds", "room", None)
privatesound = prof.settings_string_get("sounds", "private", None)
roomlist = prof.settings_string_list_get("sounds", "rooms")
if chatsound or roomsound or privatesound:
if enabled:
prof.cons_show("Sounds: ON")
else:
prof.cons_show("Sounds: OFF")
if chatsound:
prof.cons_show(" Chat : " + chatsound)
if roomsound:
prof.cons_show(" Room : " + roomsound)
if roomlist and len(roomlist) > 0:
for room in roomlist:
prof.cons_show(" " + room)
else:
prof.cons_show(" All rooms")
if privatesound:
prof.cons_show(" Private : " + privatesound)
else:
prof.cons_show("No sounds set.")
return
if arg1 == "on":
prof.settings_boolean_set("sounds", "enabled", True)
prof.cons_show("Sounds enabled")
return
if arg1 == "off":
prof.settings_boolean_set("sounds", "enabled", False)
prof.cons_show("Sounds disabled")
return
if arg1 == "set":
if arg2 == "chat":
prof.settings_string_set("sounds", "chat", arg3)
prof.cons_show("Set chat sound: " + arg3)
elif arg2 == "room":
prof.settings_string_set("sounds", "room", arg3)
prof.cons_show("Set room sound: " + arg3)
elif arg2 == "private":
prof.settings_string_set("sounds", "private", arg3)
prof.cons_show("Set private sound: " + arg3)
else:
prof.cons_bad_cmd_usage("/sounds")
return
if arg1 == "clear":
if arg2 == "chat":
prof.settings_string_set("sounds", "chat", "")
prof.cons_show("Removed chat sound.")
elif arg2 == "room":
prof.settings_string_set("sounds", "room", "")
prof.cons_show("Removed room sound.")
elif arg2 == "private":
prof.settings_string_set("sounds", "private", "")
prof.cons_show("Removed private sound.")
else:
prof.cons_bad_cmd_usage("/sounds")
return
if arg1 == "rooms":
if arg2 == "add":
if not arg3:
prof.cons_bad_cmd_usage("/sounds")
else:
prof.settings_string_list_add("sounds", "rooms", arg3)
prof.cons_show("Sounds enabled for room: " + arg3)
elif arg2 == "remove":
if not arg3:
prof.cons_bad_cmd_usage("/sounds")
else:
prof.settings_string_list_remove("sounds", "rooms", arg3)
roomlist = prof.settings_string_list_get("sounds", "rooms")
if roomlist and len(roomlist) > 0:
prof.cons_show("Sounds disabled for room: " + arg3)
else:
prof.cons_show("Empty room list for sounds, playing in all rooms.")
elif arg2 == "clear":
prof.settings_string_list_clear("sounds", "rooms")
prof.cons_show("Cleared sounds room list, playing in all rooms.")
else:
prof.cons_bad_cmd_usage("/sounds")
return
prof.cons_bad_cmd_usage("/sounds")
def prof_init(version, status, account_name, fulljid):
synopsis = [
"/sounds",
"/sounds on|off",
"/sounds set chat <file>",
"/sounds set room <file>",
"/sounds set private <file>",
"/sounds clear chat",
"/sounds clear room",
"/sounds clear private",
"/sounds rooms add <roomjid>"
]
description = "Play mp3 sounds on various Profanity events. Calling with no args shows current sound files."
args = [
[ "on|off", "Enable or disable playing sounds." ],
[ "set chat <file>", "Path to mp3 file to play on chat messages." ],
[ "set room <file>", "Path to mp3 file to play on room messages." ],
[ "set private <file>", "Path to mp3 file to play on private room messages." ],
[ "clear chat", "Remove the sound for chat messages." ],
[ "clear room", "Remove the sound for room messages." ],
[ "clear private", "Remove the sound for private messages." ],
[ "rooms add <roomjid>", "Add the room to the list that will play the room sound."],
[ "rooms remove <roomjid>", "Remove the room from the list that will play the room sound."],
[ "rooms clear", "Clear the room list, all rooms will play sounds on new messages."]
]
examples = [
"/sounds set chat ~/sounds/woof.mp3",
"/sounds set room ~/sounds/meow.mp3",
"/sounds set private ~/sounds/shhh.mp3",
"/sounds remove private",
"/sounds rooms add myroom@conference.server.org",
"/sounds on"
]
prof.register_command("/sounds", 0, 3, synopsis, description, args, examples, _cmd_sounds)
prof.completer_add("/sounds", [ "set", "clear", "on", "off", "rooms" ])
prof.completer_add("/sounds set", [ "chat", "room", "private" ])
prof.completer_add("/sounds clear", [ "chat", "room", "private" ])
prof.completer_add("/sounds rooms", [ "add", "remove", "clear" ])
def prof_post_chat_message_display(barejid, resource, message):
enabled = prof.settings_boolean_get("sounds", "enabled", False)
if not enabled:
return
soundfile = prof.settings_string_get("sounds", "chat", None)
if not soundfile:
return
_play_sound(soundfile)
def prof_post_room_message_display(barejid, nick, message):
enabled = prof.settings_boolean_get("sounds", "enabled", False)
if not enabled:
return
soundfile = prof.settings_string_get("sounds", "room", None)
if not soundfile:
return
roomlist = prof.settings_string_list_get("sounds", "rooms")
if roomlist and (barejid not in roomlist):
return
_play_sound(soundfile)
def prof_post_priv_message_display(barejid, nick, message):
enabled = prof.settings_boolean_get("sounds", "enabled", False)
if not enabled:
return
soundfile = prof.settings_string_get("sounds", "private", None)
if soundfile:
_play_sound(soundfile)

276
src/ai.py Normal file
View File

@@ -0,0 +1,276 @@
"""AI Chat Plugin for CProof XMPP Client
This plugin enables interaction with AI models via a dedicated chat window using the
LiteLLM library, supporting providers like xAI (Grok) and OpenAI. Commands allow setting
default models, API tokens, starting chats, clearing windows, and correcting messages.
- Requires the `litellm` library (`pip install litellm`).
- Configure API tokens with zero-data-retention agreements for maximum privacy.
- Use models like xAI's Grok for minimal data retention.
- Chat history is not persisted locally beyond the active session.
See Also:
- LiteLLM documentation: https://docs.litellm.ai/docs/
- xAI API: https://x.ai/api
"""
import threading
import time
import json
import os
import litellm
import prof
from typing import Callable, Dict, List, Optional, Tuple
# Global settings
DEFAULT_MODEL_KEY = "default_model" # Changed to avoid hardcoding model
TOKEN_KEY = "ai_tokens"
CHAT_WIN_ID: Optional[int] = None
# Global message history: {win_id: [{"role": "user"|"assistant", "content": str}, ...]}
CHAT_HISTORY: Dict[str, List[Dict[str, str]]] = {}
# Output file for safe display
OUTPUT_FILE = "/tmp/ai_output.txt"
FILE_LOCK = threading.Lock()
# Privacy settings for LiteLLM
litellm.drop_params = True
litellm.set_verbose = False
# Disable printing in the console to avoid breaking profanity's outline
litellm.suppress_debug_info = True
def _get_tokens() -> Dict[str, str]:
"""Retrieve tokens from settings as a dictionary."""
tokens: Dict[str, str] = {}
token_strings = prof.settings_string_list_get("ai_plugin", TOKEN_KEY)
if not token_strings:
return tokens
for ts in token_strings:
if ":" in ts:
company, token = ts.split(":", 1)
tokens[company] = token
return tokens
def _save_tokens(tokens: Dict[str, str]) -> None:
"""Save tokens to settings as a list of 'company:token' strings."""
prof.settings_string_list_clear("ai_plugin", TOKEN_KEY)
for company, token in tokens.items():
prof.settings_string_list_add("ai_plugin", TOKEN_KEY, f"{company}:{token}")
def display_settings() -> None:
"""Show the current default model and registered tokens."""
model = get_default_model()
tokens = _get_tokens()
token_info = ", ".join([f"{company}: {token[:4]}..." for company, token in tokens.items()])
prof.cons_show(f"AI Settings: Default Model: {model}, Tokens: {token_info or 'None set'}")
def set_model(model) -> None:
"""Set the default model to be used for AI chats."""
set_default_model(model)
prof.cons_show(f"Default model set to: {model}")
def run_completion(window_id: str, model: str, history: list, outfile: str, tokens: dict, file_lock) -> None:
"""Run AI completion and write response to file."""
try:
response = litellm.completion(
model=model,
messages=history,
api_key=tokens.get(model.split("/")[0], None),
).choices[0].message.content
with file_lock:
with open(outfile, "a") as f:
json.dump({"win_id": window_id, "content": f"AI: {response}"}, f)
f.write("\n")
except Exception as e:
with file_lock:
with open(outfile, "a") as f:
json.dump({"win_id": window_id, "content": f"Error: {str(e)}"}, f)
f.write("\n")
def handler(win_id: str, message: str) -> None:
"""Process messages in a chat window using the model from the window title.
Args:
win_id: Identifier for the chat window, used to extract the model name.
message: The user's message to process.
"""
model = win_id.split(" - ", 2)[1]
prof.win_show(win_id, f"Me: {message}")
# Create a new history list for this call to avoid modifying shared state
current_history = CHAT_HISTORY.get(win_id, []) + [{"role": "user", "content": message}]
thread = threading.Thread(target=run_completion, args=(win_id, model, current_history, OUTPUT_FILE, _get_tokens(), FILE_LOCK))
thread.start()
def process_queued_outputs() -> None:
"""Process outputs from the file using prof.win_show."""
if os.path.exists(OUTPUT_FILE):
with open(OUTPUT_FILE, "r") as f:
lines = f.readlines()
outputs = []
for line in lines:
if line.strip():
try:
data = json.loads(line)
outputs.append((data["win_id"], data["content"]))
except json.JSONDecodeError:
pass
for win_id, content in outputs:
prof.win_show(win_id, content)
open(OUTPUT_FILE, "w").close() # truncate the file
def create_chat_window(model: str) -> str:
"""Create a new window for AI chat with the specified model."""
win_id = f"AI Chat - {model} - {int(time.time())}"
prof.win_create(win_id, handler)
prof.win_show(win_id, f"Chat started with {model}. Type a message to interact.")
prof.win_focus(win_id)
return win_id
def start_chat(model: Optional[str] = None) -> None:
"""Open a new chat window with the specified or default model."""
model = model if model else get_default_model()
create_chat_window(model)
prof.cons_show(f"Started AI chat with model: {model}")
def set_token(company: str, token: str) -> None:
"""Store an API token for a specific company."""
tokens = _get_tokens()
tokens[company] = token
_save_tokens(tokens)
prof.cons_show(f"Token set for {company}")
def clear_chat() -> None:
"""Notify that the current chat window is cleared."""
prof.cons_show("Sorry, current API doesn't support cleaning windows")
return
win_id = prof.get_current_win()
if win_id:
prof.win_show(win_id, "Chat cleared.")
else:
prof.cons_show("No active chat window.")
def correct_message(corrected_text: str) -> None:
"""Replace the latest user message in the current window's history and get AI response."""
prof.cons_show("Sorry, current API doesn't support correcting messages and getting current window")
return
# work in progress (get_current_win doesn't work)
win_id = prof.get_current_win()
if not win_id:
prof.cons_show("No active chat window. Use /ai start <model> first.")
return
title = prof.get_win_title(win_id)
if not title or not title.startswith("AI Chat - "):
prof.cons_show("Invalid chat window. Use /ai start <model> to create one.")
return
model = title.split(" - ", 2)[1]
history = CHAT_HISTORY.get(win_id, [])
user_messages = [msg for msg in history if msg["role"] == "user"]
if not user_messages:
prof.cons_show("No user messages in this chat to correct.")
return
# Replace the latest user message
for msg in history[::-1]:
if msg["role"] == "user":
msg["content"] = corrected_text
break
handler(win_id, corrected_text)
def get_default_model() -> str:
"""Retrieve the default model from settings, defaulting to gpt-5.0."""
return prof.settings_string_get("ai_plugin", DEFAULT_MODEL_KEY, "openai/gpt-5.0")
def set_default_model(model: str) -> None:
"""Save the default model to settings."""
prof.settings_string_set("ai_plugin", DEFAULT_MODEL_KEY, model)
def _cmd_ai(*args: Tuple[Optional[str], ...]) -> None:
"""Handle /ai commands for interacting with AI models.
Synopsis:
/ai
/ai set model <model>
/ai start [<model>]
/ai set token <company> <token>
/ai clear
/ai correct <message>
"""
if not args:
display_settings()
return
if args[0] == "set" and len(args) >= 2:
if args[1] == "model" and len(args) == 3:
set_model(args[2])
elif args[1] == "token" and len(args) == 3:
try:
company, token = args[2].split(" ", 1)
set_token(company, token)
except ValueError:
prof.cons_show("Invalid format, use: /ai set token <company> <token>")
else:
prof.cons_show("Invalid command, use: /ai set model|token")
elif args[0] == "start" and len(args) <= 3:
start_chat(args[1] if len(args) == 2 else None)
elif args[0] == "clear" and len(args) == 1:
clear_chat()
elif args[0] == "correct" and len(args) == 3:
correct_message(args[2])
else:
prof.cons_bad_cmd_usage("/ai")
def prof_init(version, status, account_name, fulljid):
"""Initialize the AI chat plugin and register commands."""
synopsis = [
"/ai",
"/ai set model <model>",
"/ai start [<model>]",
"/ai set token <company> <token>",
"/ai clear",
"/ai correct <message>",
]
description = """Interact with AI models via a chat interface using LiteLLM.
You can see the list of available models here: https://models.litellm.ai/"""
args = [
["", "Display current AI plugin settings"],
["set model <model>", "Set the default AI model (e.g., gpt-3.5-turbo)"],
["start [<model>]", "Start a new AI chat with the specified or default model"],
["set token <company> <token>", "Set an API token for a specific company"],
["clear", "Clear the current AI chat window"],
["correct <message>", "Correct a message using the current AI model"],
]
examples = [
"/ai",
"/ai set token openai sk-xxx",
"/ai set model gpt-4",
"/ai start xai/grok-4",
"/ai clear",
'/ai correct I has a error',
]
prof.register_command("/ai", 0, 3, synopsis, description, args, examples, _cmd_ai)
prof.completer_add("/ai", ["set", "start", "clear", "correct"])
prof.completer_add("/ai set", ["model", "token"])
prof.completer_add("/ai set model", ["openai/gpt-5", "xai/grok-3-mini"])
prof.completer_add("/ai start", ["xai/grok-4", "openai/gpt-4o"])
prof.completer_add("/ai set token", ["openai", "xai"])
prof.register_timed(process_queued_outputs, 1) # 1s interval to process AI message output

549
src/antispam.py Normal file
View File

@@ -0,0 +1,549 @@
"""
Antispam plugin for CProof.
Asks a question from new contacts to verify their humanity.
"""
import math
import prof
import re
import datetime
from uuid import uuid4
# Constants
_antispam_defaults = {
'msg': 'Please, solve the captcha',
'question': 'Where a bullet must be lodged in order for rifle to shoot?',
'answer': 'Chamber',
'donemsg': 'You passed antispam. Congratulations!',
'blockmsg': 'You\'ve been blocked. Try again {} minutes later',
'errormsg': 'Wrong answer. Please, try again. The question is',
'otrmsg': 'Please, turn off the OTR and then send the answer.',
'unbanmsg': 'Your ban has been lifted. You can try again.',
'maxtries': 5,
'blocktime': 5,
'debug': 'off',
'dry': 'off'
}
_time_format = "%d.%m.%Y %H:%M:%S"
_roster = []
_page_limit = 10
_egg_counter = 0
_as_settings_strings = ['msg', 'question', 'answer', 'donemsg', 'blockmsg', 'otrmsg', 'errormsg', 'unbanmsg', 'debug', 'dry']
_as_settings_ints = {'maxtries': {'min': 2, 'max': 1000}, 'blocktime': {'min': -1, 'max': 100000}}
_as_settings = [*_as_settings_strings, *_as_settings_ints]
# Handlers
def prof_init(version, status, account_name, fulljid):
synopsis = [
"/antispam on|off",
"/antispam settings",
*[f"/antispam set {x} <msg>" for x in _as_settings_strings],
"/antispam set maxtries <tries>",
"/antispam set blocktime <minutes>",
"/antispam counter [<page|clear>]",
"/antispam history [<page|clear>]",
"/antispam blocklist [<page>|add|remove <jid or part of jid>|clear]",
"/antispam whitelist [<page>|add|remove <jid or part of jid>|clear]",
]
description = "Prevents spam messages by asking new contacts questions."
args = [
[ "set msg <msg>", "Set captcha message" ],
[ "set question <msg>", "Set custom captcha question" ],
[ "set answer <msg>", "Set answer to captcha question" ],
[ "set donemsg <msg>", "Set congratulation message for passing antispam" ],
[ "set blockmsg <msg>", "Set message that user is blocked. Use \"{}\" to show amounts of minutes before unlock. Example: You are banned. Unban in {} minutes." ],
[ "set otrmsg <msg>", "Set answer in case if unauthorized user tries to initiate OTR session." ],
[ "set errormsg <msg>", "Set prefix to answer in case if wrong answer given." ],
[ "set unbanmsg <msg>", "Set message given when temporary ban is finished. Set to \"None\" to prevent sending message." ],
[ "set maxtries <tries>", "Set maximum amount of tries before ban. Set -1 to make unlimited." ],
[ "set blocktime <minutes>", "Set time to block user for. Set -1 to make ban unlimited." ],
[ "set debug <on|off>", "Set debug mode on|off." ],
[ "set dry <on|off>", "Set dry mode on|off. In dry mode antispam doesn't block messages, mode is needed for debug use." ],
[ "counter [<page>|clear]", "Show or clear wrong answer counter" ],
[ "history [<page>|clear]", "Show or clear history" ],
[ "blocklist [add|remove <jid or part of jid>|clear]", "Show banned users, or add|remove user from ban" ],
[ "whitelist [add|remove <jid or part of jid>|clear]", "Whitelist users. Whitelist have priority over bans." ],
]
examples = [
"/antispam on",
"/antispam set msg Please, solve the captcha.",
"/antispam set question 2 + 2? (1984)",
"/antispam set answer 5",
"/antispam set donemsg You passed antispam. Now your messages are delivered.",
"/antispam set blockmsg You've been blocked by antispam. Please, try again later in {} minute(s).",
"/antispam set maxtries 5",
"/antispam set blocktime 69",
"/antispam blocklist clear",
"/antispam history 42",
]
prof.register_command("/antispam", 1, 40, synopsis, description, args, examples, _cmd_antispam)
prof.register_command("/as", 1, 40, _prep_alias(synopsis), description, args, _prep_alias(examples), _cmd_antispam)
prof.completer_add("/antispam",
[ "set", "history", "blocklist", "whitelist" ]
)
prof.completer_add("/antispam set",
_as_settings
)
prof.completer_add("/antispam history",
[ 'clear' ]
)
prof.completer_add("/antispam blocklist",
[ 'add', 'remove', 'clear' ]
)
prof.completer_add("/antispam whitelist",
[ 'add', 'remove', 'clear' ]
)
prof.register_timed(_update_banned_list, 60)
_force_update_roster((fulljid.split('/')[0] if fulljid else ''))
prof.cons_show("[AntiSpam] plugin started.")
if _is_dry_mode():
prof.cons_show("[AntiSpam] WARNING: dry-mode is on, no messages are going to be blocked.")
def prof_on_message_stanza_send(stanza):
body = _parse_tag_content(stanza, 'body')
if body and (to := _parse_attr(stanza, 'to').split('/', 1)[0]):
if not _is_authorized(to):
prof.settings_string_list_add("antispam", "authorized", to)
return stanza
def prof_on_presence_stanza_send(stanza):
if _parse_attr(stanza, 'type') == 'subscribe' and (to := _parse_attr(stanza, 'to').split('/', 1)[0]):
if not _is_authorized(to):
prof.settings_string_list_add("antispam", "authorized", to)
return stanza
def prof_on_message_stanza_receive(stanza) -> bool:
return _antispam_check_wrapper(stanza, 'msg')
def prof_on_presence_stanza_receive(stanza) -> bool:
return _antispam_check_wrapper(stanza, 'presence')
def prof_on_iq_stanza_receive(stanza) -> bool:
if 'jabber:iq:roster' in stanza and 'type="result"' in stanza:
_update_roster(stanza)
return _antispam_check_wrapper(stanza, 'iq')
# Custom functions
def _cmd_antispam(*args):
if not len(args):
_show_cmd_error()
if args[0] == "on":
prof.settings_boolean_set("antispam", "enabled", True)
prof.cons_show("Antispam is enabled.")
return
if args[0] == "off":
prof.settings_boolean_set("antispam", "enabled", False)
prof.cons_show("Antispam is disabled.")
return
if args[0] == "set":
if len(args) < 3:
_show_cmd_error('More arguments required')
return
key = args[1]
if key not in _as_settings:
_show_cmd_error(f'Wrong setting. You can use only {_as_settings}')
return
if args[2] == "default":
_set_default_values(key)
return
if _set_values(key, args[2:]):
prof.cons_show(f'Antispam setting {key} was successfully changed to "{" ".join(args[2:])}"')
else:
_show_cmd_error("Something went wrong, setting wasn't changed")
return
if args[0] == "settings":
prof.cons_show("===== Antispam settings =====")
for key in _as_settings:
value = _get_string_setting(key) if key in _as_settings_strings else _get_int_setting(key)
fvalue = f'"{value}"' if key in _as_settings_strings else value
prof.cons_show(f'{key} is {fvalue}')
return
if args[0] in ["counter", "history", "authorized"]:
return _cmd_show_or_clear(args)
if args[0] == "blocklist":
return _cmd_as_blocklist(args)
if args[0] == "whitelist":
return _cmd_as_whitelist(args)
if args[0] == "egg":
global _egg_counter
if _egg_counter == 10:
prof.cons_show(f"Do you know that it's an antispam plugin and not an egg farm? No more eggs for you!")
return
_egg_counter += 1
prof.cons_show(f"Egg number {_egg_counter} has been planted.")
if _egg_counter == 5:
prof.cons_show(f"What do you need so many eggs for?")
return
if args[0] == "roster":
prof.cons_show("\n".join(_roster))
return
prof.cons_bad_cmd_usage('/antispam')
def _cmd_show_or_clear(args):
if len(args) == 1:
return _show_page(args[0])
if len(args) == 2:
if args[1] == "clear":
if prof.settings_string_list_clear("antispam", args[0]):
prof.cons_show(f'{args[0].capitalize()} has been cleared.')
else:
prof.cons_show(f'{args[0].capitalize()} is already empty.')
return
return _show_page(args[0], args[1])
prof.cons_bad_cmd_usage('/antispam')
def _cmd_as_blocklist(args):
if len(args) < 3:
return _cmd_show_or_clear(args)
if args[1] == "add":
for jid in args[2:]:
ban_time = _ban_user(jid, full_jid=False, time_limited=False)
prof.cons_show(f'JID "{jid}" has been banned {ban_time}.')
elif args[1] == "remove":
for jid in args[2:]:
if _unban_user(jid):
prof.cons_show(f'JID "{jid}" has been unbanned.')
else:
prof.cons_show(f'JID "{jid}" has not been found in the blocklist.')
def _cmd_as_whitelist(args):
if len(args) < 3:
return _cmd_show_or_clear(args)
if args[1] == "add":
for jid in args[2:]:
prof.settings_string_list_add("antispam", "whitelist", jid)
prof.cons_show(f'JID "{jid}" has been whitelisted.')
elif args[1] == "remove":
for jid in args[2:]:
if prof.settings_string_list_remove("antispam", "whitelist", jid):
prof.cons_show(f'JID "{jid}" has been removed from the whitelist.')
else:
prof.cons_show(f'JID "{jid}" has not been found in the whitelist.')
def _show_cmd_error(error="", user_mistake=True) -> None:
"""Used to shows str:error when user typed wrong input"""
if user_mistake:
prof.cons_bad_cmd_usage('/antispam')
if error:
prof.cons_show(error)
def _set_default_values(key) -> bool:
default_value = _antispam_defaults[key]
if (key in _as_settings_strings):
prof.settings_string_set("antispam", key, default_value)
elif (key in _as_settings_ints):
prof.settings_int_set("antispam", key, default_value)
prof.cons_show(f'Antispam setting {key} was successfully set to its default ({default_value}).')
def _set_values(key, value) -> bool:
"""Returns Bool True if values has been set, False is value is invalid"""
value = ' '.join(value)
if key in _as_settings_strings:
prof.settings_string_set("antispam", key, value)
return True
if key in _as_settings_ints:
min_val, max_val = _as_settings_ints[key]['min'], _as_settings_ints[key]['max']
if not _represents_int_in_range(value, min_val, max_val):
prof.cons_show(f'Value of {key} must be an integer between {min_val} and {max_val}!')
return False
prof.settings_int_set("antispam", key, int(value))
return True
return False
def _parse_tag_content(stanza, tag):
tmp = re.search(f"<{tag}.*?>([\S\s]*?)</{tag}>", stanza) # don't blame me, it works :D
try:
return tmp.group(1) if tmp else ""
except IndexError as e:
return ""
def _parse_attr(stanza, attribute) -> str:
"""Parse attribute from the first tag"""
tmp = re.search(f'^\s*<[^>]+{attribute}="([^"]+)"', stanza)
try:
return tmp.group(1) if tmp else ""
except IndexError as e:
return ""
def _parse_sender(stanza) -> str:
"""Returns sender from stanza or empty string if not found"""
if "<forwarded" in stanza:
stanza = _parse_tag_content(stanza, "forwarded")
return _parse_attr(stanza, "from").split('/', 1)[0]
def _parse_id(stanza) -> str:
"""Returns id from stanza or empty string if not found"""
return _parse_attr(stanza, "id")
def _is_dry_mode() -> bool:
return _get_string_setting("dry") == "on"
def _antispam_check_wrapper(stanza, stanza_type) -> bool:
result = _antispam_check(stanza, stanza_type)
dry_mode = _is_dry_mode()
if not result:
if _get_string_setting("debug") == "on" or dry_mode:
prof.cons_show(f"[AntiSpam{' (DryMode)' if dry_mode else ''}] Blocked stanza: {stanza}")
else:
prof.log_debug(f"[AntiSpam] Blocked stanza: {stanza}")
return result or _is_dry_mode()
def _antispam_check(stanza, stanza_type) -> bool:
"""Returns Bool True if message is not spam, False if message should be blocked (spam/unverified sender)"""
sender = _parse_sender(stanza)
stanza_id = _parse_id(stanza)
if not sender and "from=" in stanza:
prof.log_warning(f"In the following stanza From is present, but not catched: \n{stanza}")
if not _is_activated():
return True
if _is_whitelisted(sender) or _is_in_roster(sender):
return True
if _is_banned(sender):
return False
if _is_authorized(sender):
return True
if not _roster:
prof.log_debug(f"[AntiSpam] Passed message because roster was empty. Stanza: {stanza}")
return True
if stanza_type == "presence":
if re.search('^\s*<presence.+type="subscribe".+>', stanza):
_send_stanza(f'<presence type="unsubscribed" to="{sender}" />')
_send_stanza(f'<message type="chat" to="{sender}"><subject>{_get_string_setting("msg")}</subject><body>{_get_string_setting("question")}</body></message>')
_counter_inc(sender)
elif stanza_type == "iq":
if re.search('^\s*<iq[^>]+type="set"', stanza):
to = f'to="{sender}" ' if sender else ''
_send_stanza(f'<iq type="error" id="{stanza_id}" {to}/>')
return False
return True
elif stanza_type == "msg":
if _parse_attr(stanza, "type") == "error":
prof.settings_string_list_add("antispam", "history", f"{_get_time()}|{sender}|{_parse_tag_content(stanza, 'body')}")
return False
if not _is_correct_answer(stanza):
if (is_otr_requested := "OTRv2" in stanza):
_send_stanza(f'<message type="chat" to="{sender}"><body>{_get_string_setting("otrmsg")}</body></message>')
prof.settings_string_list_add("antispam", "history", f"{_get_time()}|{sender}|{_parse_tag_content(stanza, 'body')}")
wrong_tries = _get_counter(sender)
subject = f'<subject>{_get_string_setting("msg")}</subject>' if wrong_tries == 0 else ''
wrong_answer = _get_string_setting("errormsg")+'\n' if wrong_tries > 0 and not is_otr_requested else ''
_send_stanza(f'<message type="chat" to="{sender}">{subject}<body>{wrong_answer}{_get_string_setting("question")}</body></message>')
_counter_inc(sender)
return False
prof.settings_string_list_add("antispam", "authorized", sender)
_send_stanza(f'<message type="chat" to="{sender}"><body>{_get_string_setting("donemsg")}</body></message>')
return False
def _force_update_roster(myjid=None) -> None:
global _roster
if not _roster and myjid:
_roster = [myjid]
prof.send_stanza(f'<iq id="{uuid4()}" type="get"><query xmlns="jabber:iq:roster"/>\</iq>')
return
def _update_roster(stanza) -> None:
global _roster
_roster = [
*re.findall('<iq[^>]+to="([^"/]+)["/]', stanza),
*[x for x,y in re.findall('<item[^>]+jid="([^"]+?)"[^>]+subscription="([^"]+?)"', stanza) if y == "both" or y == "to"],
*[x for y,x in re.findall('<item[^>]+subscription="([^"]+?)"[^>]+jid="([^"]+?)"', stanza) if y == "both" or y == "to"],
]
def _is_activated() -> bool:
return prof.settings_boolean_get("antispam", "enabled", True)
def _update_banned_list() -> None:
ban_list = prof.settings_string_list_get("antispam", "blocklist") or []
for banned_user in ban_list:
if not (tmp := _unpack_banned_user(banned_user)):
continue
ban_time, ban_type, jid = tmp
if ban_time != "Forever" and not _is_future_time(ban_time):
unbanmsg = _get_string_setting("unbanmsg")
if unbanmsg != "None":
_send_stanza(f'<message type="chat" to="{jid}"><body>{unbanmsg}</body></message>')
prof.settings_string_list_remove("antispam", "blocklist", banned_user)
return
def _unpack_banned_user(banned_user):
"""Returns None or Tuple of banned user info in (time_banned, ban_type, jid) format"""
tmp = banned_user.split("|", 2)
if len(tmp) != 3:
prof.cons_show(f"Problem on banned user unpacking: \"{banned_user}\". Deleting...")
prof.settings_string_list_remove("antispam", "blocklist", banned_user)
return
return tmp
def _is_in_roster(jid) -> bool:
return jid in _roster
def _is_authorized(jid) -> bool:
return jid in (prof.settings_string_list_get("antispam", "authorized") or [])
def _is_banned(jid) -> bool:
ban_list = prof.settings_string_list_get("antispam", "blocklist") or []
for banned_user in ban_list:
if not (tmp := _unpack_banned_user(banned_user)):
continue
ban_time, ban_type, bjid = tmp
if ban_type == "F": # On full JID ban (e.g. "user@example.com")
if jid == bjid and (ban_time == "Forever" or _is_future_time(ban_time)):
return True
else: # On partial JID ban (e.g. "@example.com", "spammer")
if bjid in jid and (ban_time == "Forever" or _is_future_time(ban_time)):
return True
return False
def _is_whitelisted(jid) -> bool:
wl_users = (prof.settings_string_list_get("antispam", "whitelist") or [])
return any(wj in jid for wj in wl_users)
def _get_validate_count(c):
tmp = c.split('|', 1)
if len(tmp) != 2:
prof.log_warning(f'Invalid counter entry: "{c}". Removing...')
prof.settings_string_list_remove("antispam", "counter", c)
return
return int(tmp[0]), tmp[1]
def _counter_inc(jid) -> None:
counter = prof.settings_string_list_get("antispam", "counter") or []
for c in counter:
if not (tmp := _get_validate_count(c)):
continue
count, cjid = tmp
if jid == cjid:
prof.settings_string_list_remove("antispam", "counter", c)
prof.settings_string_list_add("antispam", "counter", f"{count+1}|{jid}")
if count % _get_int_setting("maxtries") == 0:
ban_time = _ban_user(jid)
blockmsg = _get_string_setting("blockmsg").replace('{}', str(ban_time))
_send_stanza(f'<message type="chat" to="{jid}"><body>{blockmsg}</body></message>')
return
prof.settings_string_list_add("antispam", "counter", f"1|{jid}")
def _get_counter(jid) -> int:
counter = prof.settings_string_list_get("antispam", "counter") or []
for c in counter:
if not (tmp := _get_validate_count(c)):
continue
count, cjid = tmp
if jid == cjid:
return count
return 0
def _show_page(table, page_num=1) -> None:
"""Shows a page from table N (table is fetched from string_list setting with name) to plugin user through cons_show."""
if not _represents_int_in_range(page_num, 1):
prof.cons_show("Use correct page number. It must be integer from 1 to 10000.")
return
page_num = int(page_num)
page = page_num-1
table_content = prof.settings_string_list_get("antispam", table) or []
table_size = len(table_content)
total_pages = math.ceil(table_size/_page_limit)
if not table_content:
return prof.cons_show(f"Nothing to display, {table} is empty")
if page_num > total_pages:
return prof.cons_show(f"Page {page_num} does not exist. There are only {total_pages} pages")
page_content = table_content[page*_page_limit:page_num*_page_limit]
if not page_content:
return prof.cons_show(f"Nothing to display, {table} on Page {page_num} is empty")
for x in page_content:
formatting = (lambda x: x)
prof.cons_show(formatting(x))
prof.cons_show(f"Page {page_num}/{total_pages}. Currently {table_size} entries.")
def _ban_user(jid, full_jid=True, time_limited=True, custom_time_limit=None):
"""Adds user to ban list"""
ban_type = "F" if full_jid else "P"
if time_limited and (ban_time := custom_time_limit if custom_time_limit else _get_int_setting("blocktime")) != -1:
ban_time_formatted = (datetime.datetime.now() + datetime.timedelta(minutes=ban_time)).strftime("%d.%m.%Y %H:%M:%S")
_unban_user(jid)
prof.settings_string_list_add("antispam", "blocklist", f"{ban_time_formatted}|{ban_type}|{jid}")
return ban_time
prof.settings_string_list_add("antispam", "blocklist", f"Forever|{ban_type}|{jid}")
return "forever"
def _unban_user(jid):
"""Removes user from ban list. True on success, otherwise false."""
ban_list = prof.settings_string_list_get("antispam", "blocklist") or []
for banned_user in ban_list:
if not (tmp := _unpack_banned_user(banned_user)):
continue
ban_time, ban_type, bjid = tmp
if bjid == jid:
return prof.settings_string_list_remove("antispam", "blocklist", banned_user)
return False
# Settings utils
def _get_string_setting(setting) -> str:
return prof.settings_string_get("antispam", setting, _antispam_defaults[setting])
def _get_int_setting(setting) -> int:
return prof.settings_int_get("antispam", setting, _antispam_defaults[setting])
# Utils
def _prep_alias(replace_list):
return [x.replace("/antispam", "/as") for x in replace_list]
def _is_correct_answer(stanza):
correct_answer = _get_string_setting('answer')
answer = _parse_tag_content(stanza, "body")
if not answer:
prof.log_warning(f'Invalid mesage stanza. Ignoring user input. Stanza: {stanza}')
return False
return answer.strip().lower() == correct_answer.strip().lower()
def _is_future_time(time) -> bool:
"""Checks formatted time and returns True if time from parameter is in the future"""
try:
ptime = datetime.datetime.strptime(time, _time_format)
except ValueError as e:
prof.log_error(f"Exception raised when time was compared (input time: {time})\n{e}")
return True
return ptime > datetime.datetime.now()
def _get_time():
"""Returns current time formatted"""
return datetime.datetime.now().strftime(_time_format)
def _represents_int_in_range(s, num_min=-1, num_max=10000) -> bool:
"""Returns True if passed number is integer and it is in provided range (from -1 to 10000 by default)"""
try:
num = int(s)
return num >= num_min and num <= num_max
except ValueError:
return False
else:
return True
def _send_stanza(stanza):
if not _is_dry_mode():
return prof.send_stanza(stanza)

View File

@@ -1,5 +1,6 @@
"""
Print something in ASCII text.
Running from the console will print the text to the console.
Running in a chat or room window will send the ASCII text

110
src/autocorrector.py Normal file
View File

@@ -0,0 +1,110 @@
"""
autocorrector - Replace common typos
My typing style is such a mess. But I invest more time in correcting it
programmatically than training myself to type better. Maybe thats the
problem? Anyway, here is a plugin for all keyboard messies like me.
"""
import prof
def prof_pre_chat_message_send(barejid, message):
enabled = prof.settings_string_get("autocorrector", "enabled", "on")
if enabled == "off":
return message
else:
replacements = prof.settings_string_list_get("autocorrector","replacements")
for replacement in replacements:
repl=replacement.split(':')
message=message.replace(repl[0],repl[1])
return message
def prof_pre_priv_message_send(barejid, nick, message):
enabled = prof.settings_string_get("autocorrector", "enabled", "on")
if enabled == "off":
return message
else:
replacements = prof.settings_string_list_get("autocorrector","replacements")
for replacement in replacements:
repl=replacement.split(':')
message=message.replace(repl[0],repl[1])
return message
def prof_pre_room_message_send(barejid, message):
enabled = prof.settings_string_get("autocorrector", "enabled", "on")
if enabled == "off":
return message
else:
replacements = prof.settings_string_list_get("autocorrector","replacements")
for replacement in replacements:
repl=replacement.split(':')
message=message.replace(repl[0],repl[1])
return message
def _cmd_autocorrector(arg1=None, arg2=None):
if arg1 == "on":
prof.settings_string_set("autocorrector", "enabled", "on")
prof.cons_show("autocorrector plugin enabled")
elif arg1 == "off":
prof.settings_string_set("autocorrector", "enabled", "off")
prof.cons_show("autocorrector plugin disabled")
elif arg1 == "clearall":
prof.settings_string_list_clear("autocorrector","replacements")
prof.cons_show("autocorrector plugin replacement list cleared.")
elif arg1 == "showlist":
replacements = prof.settings_string_list_get("autocorrector","replacements")
prof.cons_show("Replacement string list:")
for replacement in replacements:
prof.cons_show(replacement)
elif arg1 == "add":
if arg2 == None:
prof.cons_bad_cmd_usage("/autocorrector")
else:
if len(arg2.split(':')) == 2:
prof.settings_string_list_add("autocorrector","replacements",arg2)
prof.cons_show("Replacement String added.")
else:
prof.cons_show("Invalid replacement String. Please use the following format to specify replacements: pattern:replacement.")
elif arg1 == "rm":
if arg2 == None:
prof.cons_bad_cmd_usage("/autocorrector")
else:
if len(arg2.split(':')) == 2:
prof.settings_string_list_remove("autocorrector","replacements",arg2)
prof.cons_show("Replacement String removed.")
else:
prof.cons_show("Invalid replacement String. Please use the following format to specify replacements: pattern:replacement.")
else:
enabled = prof.settings_string_get("autocorrector", "enabled", "on")
if enabled == "off":
prof.cons_show("autocorrector inactive.")
else:
prof.cons_show("autocorrector active.")
def prof_init(version, status, account_name, fulljid):
synopsis = [
"/autocorrector on|off",
"/autocorrector showlist",
"/autocorrector clearall",
"/autocorrector add"
"/autocorrector rm"
]
description = "Messy typist autocorrector. Replaces common typos in your outgoing messages."
args = [
[ "on|off", "Enable/disable correction for all windows" ],
[ "showlist", "show typolist." ],
[ "add", "Add entry to typolist. Use pattern:replacement as format." ],
[ "rm", "Remove entry from typolist. Use pattern:replacement as format." ],
[ "clearall", "Remove all entries from typolist" ]
]
examples = [
"/autocorrector on",
"/autocorrector add tpyo:typo",
"/autocorrector add tset:test",
"/autocorrector rm tset:test",
"/autocorrector showlist",
]
prof.settings_string_set("autocorrector", "enabled", "on")
prof.register_command("/autocorrector", 0, 2, synopsis, description, args, examples, _cmd_autocorrector)
prof.completer_add("/autocorrector", [ "on", "off", "clearall", "add", "rm", "showlist" ])

View File

@@ -72,7 +72,7 @@ def prof_init(version, status, account_name, fulljid):
[ "<url>", "URL to open in the browser" ]
]
examples = [
"/browser http://www.profanity.im"
"/browser http://jabber.space/"
]
prof.register_command("/browser", 0, 1, synopsis, description, args, examples, _cmd_browser)

7
src/c/moj/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
moj
moj.exe
*.o
*.so
gen/lookup.c
gen/emoji.json
gen/emoji.csv

15
src/c/moj/COPYING Normal file
View File

@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2018, Adrian "vifino" Pistol
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

68
src/c/moj/Makefile Normal file
View File

@@ -0,0 +1,68 @@
# moj makefile
CC ?= cc
MOJILIST=https://raw.githubusercontent.com/milesj/emojibase/master/packages/data/en/raw.json
# Do not touch.
all: moj libmoj.so
# Generate lookup lists
gen/emoji.json:
mkdir -p gen
wget -O $@ ${MOJILIST}
gen/emoji.csv: gen/emoji.json
jq -r '.[] | select(.emoji) | [.emoji] + .shortcodes | @csv' -- \
gen/emoji.json | \
sed 's/^"//;s/"$$//' | awk -f src/process.awk > $@
gen/emoji.gperf: src/gen_gperf.sh gen/emoji.csv
./src/gen_gperf.sh > ./gen/emoji.gperf
gen/emoji_list.h: src/gen_list.sh gen/emoji.csv
./src/gen_list.sh > $@
gen/lookup.c: gen/emoji.gperf
gperf --global-table --readonly-tables \
--struct-type --includes \
--compare-lengths --compare-strncmp \
--ignore-case -Q emoji_spool \
-H emoji_name_hash -N emoji_lookup_ref -m 3 \
--output-file=$@ $^
# Compile our code.
src/moj.o: gen/lookup.c gen/emoji_list.h src/moj.c
$(CC) -c ${CFLAGS} ${CPPFLAGS} -o $@ src/moj.c
libmoj.so: src/moj.o
$(CC) -shared -fPIC ${CFLAGS} ${CPPFLAGS} -o $@ ${LDFLAGS} $<
moj: src/main.c src/moj.o
$(CC) ${CFLAGS} ${CPPFLAGS} -o $@ ${LDFLAGS} $^
# Profanity plugin.
profanity_emoji.so: src/profanity_emoji.c src/moj.o
$(CC) -shared -fPIC ${CFLAGS} ${CPPFLAGS} -o $@ ${LDFLAGS} $^
profanity: profanity_emoji.so
# Install
install: moj
install -d $(DESTDIR)/$(PREFIX)/bin
install moj $(DESTDIR)/$(PREFIX)/bin
install-devel: libmoj.so
install -d $(DESTDIR)/$(PREFIX)/lib
install libmoj.so $(DESTDIR)/$(PREFIX)/lib
install -d $(DESTDIR)/$(PREFIX)/include
cp src/moj.h $(DESTDIR)/$(PREFIX)/include/libmoj.h
uninstall:
rm -f $(DESTDIR)/$(PREFIX)/bin/moj
uninstall-devel:
rm -f $(DESTDIR)/$(PREFIX)/lib/libmoj.so $(DESTDIR)/$(PREFIX)/include/libmoj.h
# Cleanup
clean:
rm -f moj gen/lookup.c src/*.o profanity_emoji.so

28
src/c/moj/README.md Normal file
View File

@@ -0,0 +1,28 @@
# moj
'moj is for those of us that want something more minimal than emojify.
Reasons to use moj instead of emojify:
- only 3 letters(!), huge reduction compared to 7 of emojify.
- written in C, compile time hash table generation, no interpreted language.
- VERY FAST. How does 0.002s to generate the average commit messages of this project sound? This is on a ThinkPad T61. A potato.
- data not mixed with code, only generated data/glue and handwritten code. (seperate files)
# Requirements
- Make (compile time)
- C99 compiler(?). (compile time)
- gperf (compile time)
No runtime deps.
To regen the emoji list you also need:
- wget
- jq
- awk
# Credits
[milesj/emojibase](https://github.com/milesj/emojibase) for the JSON Emoji list. Many thanks
vifino for doing everything else.
# License
ISC.

1810
src/c/moj/gen/emoji.gperf Normal file

File diff suppressed because it is too large Load Diff

1646
src/c/moj/gen/emoji_list.h Normal file

File diff suppressed because it is too large Load Diff

15
src/c/moj/src/gen_gperf.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
# Generator for the hash list.
# This is unquestionably magic.
set -e
cat <<HEADER
%{
// THIS FILE IS GENERATED BY src/gen_list.sh.
// DO NOT TOUCH HERE.
%}
struct emoji_ref { char* name; unsigned int ref; };
%%
HEADER
cat gen/emoji.csv | sed 's/"//g' | \
awk -F',' '{ count++; for (i = 2; i <= NF; i++) printf "%s, %i\n", $i, count;}'

26
src/c/moj/src/gen_list.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/sh
# Generator for the actual emoji list.
set -e
EMOJIS=$(wc -l < gen/emoji.csv)
cat <<HEADER
// THIS FILE IS GENERATED BY src/gen_list.sh.
// DO NOT TOUCH HERE.
#define NUM_EMOJI $EMOJIS
static const char* emoji_list[$EMOJIS] = {
HEADER
gen() {
printf "\t\"%s\",\n" $1
}
cat gen/emoji.csv | sed 's/,/ /g' | while read -r input; do
gen $(eval "echo $input")
done
cat <<FOOTER
};
FOOTER

124
src/c/moj/src/main.c Normal file
View File

@@ -0,0 +1,124 @@
// moj.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <getopt.h>
#include "moj.h"
#define MODE_NORMAL 0
#define MODE_LOOKUP 1
#define MODE_USELIBRARY 2
void usage(char* name, int fail) {
FILE* f = stdout;
if (fail) f = stderr;
fprintf(f, "Usage: %s [-s sep] [-e] <:emoji: text>\n"
" -e: lookup mode - interpret arguments as emoji names\n"
" -s <sep>: use custom seperator\n", name);
exit(fail);
}
int main(int argc, char* argv[]) {
if (argc < 2) {
usage(argv[0], 1);
}
// parse opts
int opt;
int mode = MODE_NORMAL;
char* sep = NULL;
while ((opt = getopt(argc, argv, "hels:")) != -1) {
switch (opt) {
case 'h':
usage(argv[0], 0);
break;
default:
case '?':
usage(argv[0], 1);
break;
case 'e':
mode = MODE_LOOKUP;
break;
case 's':
sep = optarg;
break;
case 'l':
mode = MODE_USELIBRARY;
break;
}
}
if (optind >= argc)
usage(argv[0], 1);
int argi;
switch (mode) {
case MODE_LOOKUP:
for (argi = optind; argi < argc; argi++) {
char* found = moj_lookup(argv[argi], strlen(argv[argi]));
if (found) {
printf("%s%s", found, sep ? sep : "\n");
} else {
printf("Couldn't find %s. :(\n", argv[argi]);
return 1;
}
}
if (sep) printf("\n");
break;
case MODE_NORMAL:
for (argi = optind; argi < argc; argi++) {
char* s = argv[argi];
char c;
int ci;
int len = strlen(s);
int start = 0;
int attention = 0;
for (ci = 0; ci <= len; ci++) {
c = s[ci];
if (attention) { // in :
if (c == ' ') {
attention = 0;
s[ci] = 0;
fprintf(stdout, "%s ", s + start);
} else if (c == ':') {
// check if start to ci - start -1 is a valid emoji.
s[ci] = 0;
char* found = moj_lookup(s + start + 1, ci - start - 1);
if (found) {
fprintf(stdout, "%s", found);
} else {
// not an emoji? oops.
fprintf(stdout, "%s:", s + start);
start = ci;
// the next might be one, though..
}
attention = 0;
}
} else if (c == ':' && ci < len) {
attention = 1;
start = ci;
} else {
putc(c, stdout);
}
}
if (attention && start < len)
fprintf(stdout, "%s", s + start + 1);
if (argi < (argc - 1))
fputs(sep ? sep : " ", stdout);
}
fprintf(stdout, "\n");
break;
case MODE_USELIBRARY:
for (argi = optind; argi < argc; argi++) {
char* rep = moj_replace(argv[argi], strlen(argv[argi]));
printf("%s\n", rep);
free(rep);
}
break;;
}
return 0;
}

98
src/c/moj/src/moj.c Normal file
View File

@@ -0,0 +1,98 @@
// moj: Small Emoji library
// Based on our generated things.
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "../gen/lookup.c"
#include "../gen/emoji_list.h"
const char* moj_lookup(const char* name, size_t len) {
const struct emoji_ref *found = emoji_lookup_ref(name, len);
if (found) {
return emoji_list[found->ref - 1];
}
return NULL;
}
char* moj_replace(const char* text, size_t len) {
if (!len)
return strdup(text);
char c;
// count size delta to alloc a string just the right size.
int size_delta = 0;
size_t ci;
size_t start = 0;
size_t strend;
int attention = 0;
for (ci = 0; ci <= len; ci++) {
c = text[ci];
if (attention) { // in :
if (c == ' ') {
attention = 0;
} else if (c == ':') {
// check if start to ci - start -1 is a valid emoji.
size_t elen = ci - start - 1;
const char* found = moj_lookup(text + start + 1, elen);
if (found) {
size_delta += elen - strlen(found);
} else {
start = ci;
}
attention = 0;
}
} else if (c == ':' && ci < len) {
attention = 1;
start = ci;
}
}
// allocate buffer the right size.
size_t realsz = len + size_delta;
char* buf = malloc(realsz + 1);
if (!buf)
return NULL;
start = 0;
attention = 0;
size_t offset = 0;
for (ci = 0; ci < len; ci++) {
c = text[ci];
if (attention) { // in :
if (c == ' ') {
attention = 0;
memcpy(buf + offset, text + start, ci - start + 1);
offset += ci - start + 1;
} else if (c == ':') {
// check if start to ci - start -1 is a valid emoji.
const char* found = moj_lookup(text + start + 1, ci - start - 1);
if (found) {
offset += sprintf(buf + offset, "%s", found);
} else {
// not an emoji? oops.
memcpy(buf + offset, text + start, ci - start + 1);
offset += ci - start + 1;
start = ci;
// the next might be one, though..
}
attention = 0;
}
} else if (c == ':' && ci < len) {
attention = 1;
start = ci;
} else {
buf[offset] = c;
offset++;
}
}
if (attention) {
memcpy(buf + offset, text + start, ci - start);
offset += ci - start;
}
buf[realsz] = 0;
return buf;
}

5
src/c/moj/src/moj.h Normal file
View File

@@ -0,0 +1,5 @@
// moj: a small emoji library and cli.
#include <stddef.h>
char* moj_lookup(const char* name, size_t len);
char* moj_replace(const char* name, size_t len);

93
src/c/moj/src/process.awk Normal file
View File

@@ -0,0 +1,93 @@
#!/usr/bin/awk -f
# This file takes in our CSV, emits simpler stuff.
# It also fixes up some things.
# Bunch of convenience functions.
function didsee(name) {
if (name in seen)
return 1;
return 0;
}
function hasnew() {
for (i = 2; i <= NF; i++)
if (!didsee($i))
return 1;
return 0;
}
function newname(name) {
if (didsee(name))
return 0;
seen[name] = 1;
return 1;
}
function char(str, idx) {
return substr(str, idx, 1);
}
function prefixed(name, prefixes) {
for (prefix in prefixes)
if (index(name, prefixes[prefix]) == 1)
return 1;
return 0;
}
function inarray(name, array) {
for (i in array)
if (array[i] == name)
return 1;
return 0;
}
function noing(name) {
# yep thats the name.
len = length(name);
if (len > 5 && substr(name, len - 2, 3) == "ing" && !prefixed(name, persons) && !inarray(name, exemptions)) {
if (char(name, len - 4) == char(name, len - 3)) {
# double letter combo, gg, nn, etc..
short = substr(name, 0, len - 4);
} else {
short = substr(name, 0, len - 3);
}
if (newname(short))
printf ",%s", short;
}
}
BEGIN {
FS="\",\""
# I hate this.
persons[1] = "person_";
persons[2] = "women_";
persons[3] = "woman_"
persons[4] = "men_";
persons[5] = "man_";
# I hate this even more.
exemptions[1] = "bowing";
exemptions[2] = "golfing";
}
{
if (hasnew())
printf "%s", $1;
for (i = 2; i <= NF; i++) {
if (newname($i)) {
printf ",%s", $i;
noing($i);
if (index($i, "person_") == 1) {
replaced = substr($i, 8);
if (newname(replaced)) {
printf ",%s", replaced;
}
noing(replaced);
}
}
}
printf "\n";
}

View File

@@ -0,0 +1,10 @@
// moj: CProof plugin
// Intercepts text and replaces :emoji:
#include <profapi.h>
#include <string.h>
#include "moj.h"
char* prof_pre_chat_message_send(const char* const barejid, const char* message) {
return moj_replace(message, strlen(message)); // yep that is it.
}

View File

@@ -1,5 +1,5 @@
/*
Simple plugin to display the PID of the Profanity process and its parent
Simple plugin to display the PID of the CProof process and its parent
Theme example in ~/.local/share/profanity/plugin_themes

33
src/emoticons.py Normal file
View File

@@ -0,0 +1,33 @@
"""
Converts smileys in a text to their Unicode character representations.
"""
import prof
EMOTICON_UNICODE_DICT = {':)': '😃', ':(': '☹️', ':D': '😃', ':O': '😲', ';)': '😉', ':-)': '😊', ':-D': '😁', ':-(': '😞',
';-)': '😜', '<3': '❤️', ':P': '😛', ':|': '😐', ':/': '😕', ':*': '😘', ':]': '😃', ':[': '😢',
':-|': '😐', ':\\': '😕', ':3': '😺', 'O:)': '😇', '>:(': '😠', ':poop:': '💩', ':fire:': '🔥',
':heart:': '❤️', ':thumbsup:': '👍', ':star:': '', ':beer:': '🍺', ':pizza:': '🍕', ':sunglasses:': '😎',
':ok_hand:': '👌', ':heart_eyes:': '😍'}
def replace_emoticons(text, emoticon_dict):
for emoticon, unicode_char in emoticon_dict.items():
text = text.replace(emoticon, unicode_char)
return text
def convert_emoticons(input_str):
return replace_emoticons(input_str, EMOTICON_UNICODE_DICT)
def prof_pre_chat_message_display(barejid, resource, message):
return convert_emoticons(message)
def prof_pre_room_message_display(barejid, nick, message):
return convert_emoticons(message)
def prof_pre_priv_message_display(barejid, nick, message):
return convert_emoticons(message)

167
src/history.py Normal file
View File

@@ -0,0 +1,167 @@
"""
CProof plugin to read full chat history with custom editor using /hh command.
"""
import os
import prof
import re
import sqlite3
from pathlib import Path
_editor = ""
_current_user = ""
_logs_dir = ""
_db_file = ""
_win = "History"
_plugin_name = __file__.split('/')[-1] if __file__ else "history.py"
def _handle_win_input():
pass
def _create_win(win):
if not prof.win_exists(win):
prof.win_create(win, _handle_win_input)
def _show_error(error):
error_msg = f"[History Reader] Error happened: {error}"
prof.cons_alert()
prof.cons_show(error_msg)
prof.log_error(error_msg)
return
def _str_sanitize(text):
return re.sub('[^a-zA-Z0-9\._]', '', text.replace("@", "_at_"))
def _cmd_editor(*args):
global _editor
_db_connection = sqlite3.connect(f"{_db_file}")
_db_connection.text_factory = bytes
_cur = _db_connection.cursor()
msg_buffer = []
if args and args[0] == "set":
if len(args) < 2:
prof.cons_show("Please, use this format: \"/hh set <editor>\"")
return
_editor = args[1]
prof.settings_string_set("history", "editor", _editor)
prof.cons_show(
f"New editor set up successfully. New editor: {_editor}")
return
if args and args[0] == "--no-repeat":
prof.cons_show("[History Reader] Trying operation again...")
if not _current_user:
if args and args[0] == "--no-repeat":
_show_error("Can't fetch current user.")
return
prof.send_line(f"/plugins reload {_plugin_name}")
prof.send_line("/hh --no-repeat")
return
if not _editor:
prof.cons_show(
"Please, set up editor using /hh set. E.g. /hh set /usr/bin/vim")
return
if not (recipient := prof.get_current_recipient()):
prof.cons_show("Please, use this command in a chat window")
return
tmpfpath = _logs_dir / f"{_str_sanitize(recipient)}.log"
res = _cur.execute("""SELECT timestamp, from_jid, message FROM `chatlogs`
WHERE ((`from_jid` = :jid AND `to_jid` = :myjid)
OR (`from_jid` = :myjid AND `to_jid` = :jid))
ORDER BY id""", {"jid": recipient, "myjid": _current_user})
for bmsg in res.fetchall():
try:
msg = [x.decode("UTF-8", errors="backslashreplace") for x in bmsg]
except Exception as e:
msg = [x.decode("UTF-8", errors="replace") for x in bmsg]
sender = "me" if msg[1] == _current_user else msg[1]
msg_buffer.append(f"{msg[0]} - {sender}: {msg[2]}")
tmpfpath.write_text('\n'.join(msg_buffer),
encoding="UTF-8", errors="replace")
pid = os.fork()
if pid == 0:
os.execlp(_editor, _editor, str(tmpfpath))
else:
if pid == -1:
return
os.waitpid(pid, 0)
prof.send_line("/statusbar show name")
try:
tmpfpath.unlink()
except Exception as e:
prof.log_error(
f"[History Reader] Error on file deletion (path: {tmpfpath}): {e}")
return
def prof_on_connect(account_name, fulljid):
prof.log_debug(
f"[History Reader] prof_on_connect called with this JID: {fulljid}")
_init(fulljid)
def prof_on_disconnect(account_name, fulljid):
prof.log_debug(
f"[History Reader] prof_on_disconnect called with this JID: {fulljid}")
global _current_user
_current_user = ""
def prof_init(version, status, account_name, fulljid):
prof.log_debug(
f"[History Reader] prof_init called with this JID: {fulljid}")
_init(fulljid)
synopsis = ["/hh"]
description = "Open an editor and check user's history."
args = [
["set", "Set custom editor."]
]
examples = [
"/hh",
"/hh set /usr/bin/vim",
"/hh set gtk-open"
]
prof.register_command("/hh", 0, 2, synopsis,
description, args, examples, _cmd_editor)
prof.completer_add("/hh", ["set"])
prof.filepath_completer_add("/hh set")
def _init(fulljid):
global _current_user, _editor, _logs_dir, _db_file
prof.log_debug(
f"[History Reader] Initialization started with this JID: {fulljid}")
_current_user = fulljid and fulljid.split('/')[0]
if not _current_user:
prof.log_debug(
"[History Reader] Can't fetch current user, aborting initialization...")
return
_cproof_home = Path(os.getenv('XDG_DATA_HOME')
or "~/.local/share").expanduser() / "profanity"
_logs_dir = _cproof_home / "chatlogs" / _str_sanitize(_current_user)
db_dir = _cproof_home / "database" / _str_sanitize(_current_user)
if not _logs_dir.is_dir():
_show_error(f"Can't open logs directory. Path: {_logs_dir}")
return
if not db_dir.is_dir():
_show_error(f"Can't open DB directory. Path: {db_dir}")
return
_db_file = db_dir / "chatlog.db"
if not _db_file.is_file():
_show_error(f"Log db is not present. Path: {_db_file}")
return
_editor = prof.settings_string_get("history", "editor", "")
if not _editor:
prof.cons_show("Please, set up editor using /hh set.")
else:
prof.cons_show(
f"[History Reader] Successfully started. Editor: {_editor}")

47
src/htmlstrip.py Normal file
View File

@@ -0,0 +1,47 @@
# -*- coding:utf-8 -*-
# vim: set ts=2 sw=2 sts=2 et:
"""
Removes "unwanted" HTML tags from incoming messages and unescapes html.
Author: Diego Blanco <diego.blanco@treitos.com>
Some XMPP clients like Adium or Pidgin send HTML tags like <font>, <b>, <u>, etc
to format text, specially when using OTR. This plugin removes them and also
unescapes HTML like &lt;, &gt;, etc that these clients also tend to escape.
"""
import re
import prof
import html
#( regular expressions
re_font = re.compile('</?FONT(>| [^>]+>)', re.IGNORECASE)
re_br = re.compile('</?BR>', re.IGNORECASE)
re_styles = re.compile('</?(U|B|I)>', re.IGNORECASE)
re_link = re.compile('</?a(>| [^>]+>)', re.IGNORECASE)
#)
def _htmlstrip(message):
""" Cleans message
"""
try:
_message = message
_message = re_styles.sub( '', _message )
_message = re_font.sub( '', _message )
_message = re_br.sub( '\n', _message )
_message = re_link.sub( '', _message )
_message = html.unescape( _message )
except:
return ("ERR@htmlstrip.py: " + message)
finally:
return (_message)
def prof_pre_chat_message_display(barejid, resource, message):
return _htmlstrip(message)
def prof_pre_room_message_display(barejid, nick, message):
return _htmlstrip(message)
def prof_pre_priv_message_display(barejid, nick, message):
return _htmlstrip(message)

View File

@@ -5,7 +5,6 @@ Requires scrot installed for screenshot (on linux)
sudo apt-get install scrot
Uses built in screencapture on OSX
"""
import prof
@@ -59,3 +58,4 @@ def prof_init(version, status, account_name, fulljid):
prof.register_command("/imgur", 1, 1, synopsis, description, args, examples, _cmd_imgur)
prof.completer_add("/imgur", [ "screenshot" ])
prof.filepath_completer_add("/imgur")

137
src/notifycmd.py Normal file
View File

@@ -0,0 +1,137 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Executes a command when a message is received
MIT License
Copyright (c) 2016 Oliver Schmidhauser
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import prof
from subprocess import Popen
import time
from sys import platform
def secure(string):
string=string.replace('\\','\\\\')
string=string.replace("\"","\\\"")
string=string.replace("$","\$")
string=string.replace("`","\`")
return string
def notifycmd(sender,message):
command = prof.settings_string_get("notifycmd", "command", "")
# replace markers with complex strings first to avoid "%%mittens"=>"(%%)mittens"=>"(%m)ittens"=>"<message>ittens" but rather do "(%%)mittens"=>"%mittens"
command = command.replace("%%","{percentreplace}")
command = command.replace("%s","${senderreplace}")
command = command.replace("%m","${messagereplace}")
command = command.replace("{percentreplace}","%")
command = "set -f;senderreplace=\"{}\";messagereplace=\"{}\";{}".format(secure(sender),secure(message),command)
p = Popen(['sh', '-c', command])
def prof_post_chat_message_display(barejid, resource, message):
enabled = prof.settings_string_get("notifycmd", "enabled", "on")
current_recipient = prof.get_current_recipient()
if enabled == "on" or (enabled == "active" and current_recipient == barejid):
notifycmd(barejid, message)
return message
def prof_post_room_message_display(barejid, nick, message):
enabled = prof.settings_string_get("notifycmd", "enabled", "on")
rooms = prof.settings_string_get("notifycmd", "rooms", "mention")
current_muc = prof.get_current_muc()
mynick = prof.get_room_nick(barejid)
# Don't notify for ones own messages
if nick != mynick:
if rooms == "on":
if enabled == "on":
notifycmd(nick + " in " + barejid, message)
elif enabled == "active" and current_muc == barejid:
notifycmd(nick, message)
elif rooms == "mention":
if mynick in message:
if enabled == "on":
notifycmd(nick, message)
elif enabled == "active" and current_muc == barejid:
notifycmd(nick, message)
return message
def prof_post_priv_message_display(barejid, nick, message):
if enabled:
notifycmd(nick,message)
return message
def _cmd_notifycmd(arg1=None, arg2=None):
if arg1 == "on":
prof.settings_string_set("notifycmd", "enabled", "on")
prof.cons_show("Notifycmd plugin enabled")
elif arg1 == "off":
prof.settings_string_set("notifycmd", "enabled", "off")
prof.cons_show("Notifycmd plugin disabled")
elif arg1 == "active":
prof.settings_string_set("notifycmd", "enabled", "active")
prof.cons_show("Notifycmd plugin enabled for active window only")
elif arg1 == "command":
if arg2 == None:
prof.cons_bad_cmd_usage("/notifycmd")
else:
prof.settings_string_set("notifycmd", "command", arg2)
prof.cons_show("notifycmd plugin command set to: " + arg2)
elif arg1 == "rooms":
if arg2 == None:
prof.cons_bad_cmd_usage("/notifycmd")
else:
prof.settings_string_set("notifycmd", "rooms", arg2)
prof.cons_show("notifycmd plugin notifications for rooms set to: " + arg2)
else:
enabled = prof.settings_string_get("notifycmd", "enabled", "on")
rooms = prof.settings_string_get("notifycmd", "rooms", "mention")
command = prof.settings_string_get("notifycmd", "command", "")
prof.cons_show("Notifycmd plugin settings:")
prof.cons_show("enabled : " + enabled)
prof.cons_show("command : " + command)
prof.cons_show("rooms : " + rooms)
def prof_init(version, status, account_name, fulljid):
synopsis = [
"/notifycmd on|off|active",
"/notifycmd command <command>",
"/notifycmd rooms on|off|mention"
]
description = "Executes a command when a message is received"
args = [
[ "on|off", "Enable/disable notifycmd for all windows" ],
[ "active", "Enable notifycmd for active window only" ],
[ "command <args>", "Set command to execute. Replaces %s with sender, %m with message and %% with literal %." ],
[ "rooms <args>", "Setting for multi-user rooms. Set to 'on', 'off' or 'mention'. If set to mention it will only run it your nick was mentioned." ]
]
examples = []
prof.register_command("/notifycmd", 0, 2, synopsis, description, args, examples, _cmd_notifycmd)
prof.completer_add("/notifycmd", [ "on", "off","active","command","rooms" ])
prof.completer_add("/notifycmd rooms", [ "on", "off", "mention" ])

60
src/paste.py Normal file
View File

@@ -0,0 +1,60 @@
"""
Paste the contents of the clipboard when in a chat or room window.
Dependencies:
pip install future
sudo apt-get python-tk (or python3-tk)
"""
import prof
import tkinter as tk
def _cmd_paste(arg1=None, arg2=None):
if not arg1:
root = tk.Tk(baseName="")
root.withdraw()
result = root.clipboard_get()
newline = prof.settings_boolean_get("paste", "newline", True)
if len(result.splitlines()) > 1 and newline:
prof.send_line(u'\u000A' + result)
else:
prof.send_line(result)
return
if arg1 == "newline":
if not arg2:
prof.cons_show("")
newline = prof.settings_boolean_get("paste", "newline", True)
if newline:
prof.cons_show("paste.py newline: on")
else:
prof.cons_show("paste.py newline: off")
elif arg2 == "on":
prof.settings_boolean_set("paste", "newline", True)
prof.cons_show("paste.py newline enabled.")
elif arg2 == "off":
prof.settings_boolean_set("paste", "newline", False)
prof.cons_show("paste.py newline disabled.")
else:
prof.cons_bad_cmd_usage("/paste")
return
prof.cons_bad_cmd_usage("/paste")
def prof_init(version, status, account_name, fulljid):
synopsis = [
"/paste",
"/paste newline on|off"
]
description = "Paste contents of clipboard."
args = [
[ "newline on|off", "Send newline before multiline clipboard text, defaults to on" ]
]
examples = []
prof.register_command("/paste", 0, 2, synopsis, description, args, examples, _cmd_paste)
prof.completer_add("/paste", [ "newline" ])
prof.completer_add("/paste newline", [ "on", "off" ])

144
src/sanitize.py Normal file
View File

@@ -0,0 +1,144 @@
"""
Sanitize incoming messages from Adium clients.
MIT License
Copyright (c) 2016 Nd2(SO4)3
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from io import StringIO
from lxml import etree
from lxml.etree import _ElementUnicodeResult, _ElementStringResult
import prof
# ROADMAP
#
# Version 2:
# "/sanitise on|off": global auto detect setting
# "/sanitise <jid> on|off|<mode>": setting per JID: can be autodetect, off,
# or a special "mode" (if there are any).
iq_count = 1
# Stores client information. JID/str~>Client/str.
clients = {}
def prof_on_presence_stanza_receive(stanza):
"""Send a software version request when a contact comes online."""
full_jid = etree.fromstring(stanza).get("from")
if full_jid:
send_version_request(full_jid)
return True
def send_version_request(full_jid):
global iq_count
iq = etree.Element(
"iq", {"type": "get", "to": full_jid, "id": "sanitiser_" + str(iq_count)}
)
etree.SubElement(iq, "query", {"xmlns": "jabber:iq:version"})
iq_count = iq_count + 1
prof.send_stanza(etree.tostring(iq))
def prof_on_iq_stanza_receive(stanza):
"""Listen for software version responses and save results in the
'clients' store.
"""
iq = etree.fromstring(stanza)
id = iq.get("id")
if not id:
return True
if not id.startswith("sanitiser_"):
return True
full_jid = iq.get("from")
if not full_jid:
return False
query = iq.find("{jabber:iq:version}query")
if query is None:
return False
client = query.find("{jabber:iq:version}name")
if client is not None:
clients[full_jid] = client.text
return False
def prof_pre_chat_message_display(jid, resource, message):
"""Sanitises chat room messages if necessary."""
full_jid = "%s/%s" % (jid, resource)
if full_jid not in clients:
return message
return get_sanitiser(clients[full_jid])(message)
# Sanitiser functions:
def get_sanitiser(client):
"""Returns appropriate sanitiser function for the given
Jabber client (string).
"""
if client.startswith("Adium"):
return sanitise
else:
return lambda msg: msg
def sanitise(message):
"""Cleans message from HTML. Raises exceptions on problems."""
parser = etree.HTMLParser()
tree = etree.parse(StringIO(unicode(message)), parser)
elems = tree.xpath("(//br|//a|//*[not(self::a)]/text())")
return "".join(map(substitude, elems))
def try_sanitise(message):
"""Same as 'sanitise' but returns unmodified message if it encounters
a problem, instead of throwing an exception.
"""
try:
return sanitise(message)
except:
return message
def substitude(elem):
"""Substitudes an lxml element with a string representation."""
if type(elem) in (_ElementUnicodeResult, _ElementStringResult):
return str(elem)
else:
if elem.tag == "br":
return "\n"
elif elem.tag == "a":
text = elem.text or ""
href = elem.get("href", "")
if not text and not href:
return ""
elif text == href:
return href
else:
return "%s (%s)" % (text, href)
else:
raise Exception("unhandled tag <%s>" % elem.tag)

View File

@@ -9,15 +9,16 @@ On OSX will use the built in 'say' command.
import prof
import os
import shlex
from sys import platform
def say(message):
args = prof.settings_string_get("say", "args", "")
if platform == "darwin":
os.system("say " + args + " '" + message + "' 2>/dev/null")
os.system("say " + args + " " + shlex.quote(message) + " 2>/dev/null")
elif platform == "linux" or platform == "linux2":
os.system("echo '" + message + "' | espeak " + args + " 2>/dev/null")
os.system("echo " + shlex.quote(message) + " | espeak " + args + " 2>/dev/null")
def prof_post_chat_message_display(barejid, resource, message):

128
src/shortcuts.py Normal file
View File

@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
"""
Manage Message Shortcuts
The MIT License (MIT)
Copyright (c) 2016 René Calles
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import json
import re
import prof
ENABLED = True
SHORTCUTS_DICTIONARY = {u'shrug': u'¯\_(ツ)_/¯',
u'yay': u'\o/'}
plugin_win = "Shortcuts"
def _handle_win_input(win, line):
prof.win_show(win, line)
def create_win():
if prof.win_exists(plugin_win) == False:
prof.win_create(plugin_win, _handle_win_input)
def _substitute(message):
if ENABLED:
shortcuts = re.findall(r'(?:^|\s)(:\w+:)(?=\s|$)', message)
for s in shortcuts:
shortcut_text = SHORTCUTS_DICTIONARY.get(s.strip()[1:-1])
if shortcut_text:
message = message.replace(s.strip(), shortcut_text)
return message
def save(key, value):
global SHORTCUTS_DICTIONARY
SHORTCUTS_DICTIONARY[key] = value
def _list_shortcuts():
global SHORTCUTS_DICTIONARY
# ordered_dict = sorted(DICTIONARY.items(), key=lambda t: t[0])
prof.cons_show(u'Shortcuts: ')
for key, value in SHORTCUTS_DICTIONARY.iteritems():
prof.cons_show(u':{0}: => {1}'.format(key, value))
def _cmd_shortcuts(arg1=None, arg2=None, arg3=None):
global ENABLED
if arg1 == None:
prof.cons_show("Shortcuts Plugin is {}.".format("ON" if ENABLED else "OFF"))
elif arg1 == "on":
ENABLED = True
elif arg1 == "off":
ENABLED = False
elif arg1 == "list":
_list_shortcuts()
elif arg1 == "set":
if arg2 == None:
prof.cons_bad_cmd_usage("/shortcuts")
else:
save(arg2, arg3)
else:
prof.cons_bad_cmd_usage("/shortcuts")
def prof_pre_chat_message_send(jid, message):
return _substitute(message)
def prof_pre_room_message_send(room, message):
return _substitute(message)
def prof_pre_priv_message_send(room, nick, message):
return _substitute(message)
def prof_init(version, status):
synopsis = [
"/shortcuts",
"/shortcuts on|off",
"/shortcuts set <name> <text>"
]
description = "Manage shortcuts for emoticons or other stuff"
args = [
[ "on|off", "Enable/disable shortcuts" ],
[ "set <name> <text>", "Save shortcut" ],
[ "list", "List Shortcuts Dictionary"]
]
examples = [
"/shortcuts",
"/shortcuts on",
"/shortcuts set lol \"Laughing out loud\""
]
prof.register_command("/shortcuts", 0, 3, synopsis, description, args, examples, _cmd_shortcuts)
prof.register_ac("/shortcuts", [ "on", "off", "set" ])
shortcut_keys = [":{}:".format(k) for k in SHORTCUTS_DICTIONARY.keys()]
# prof.cons_show(", ".join(shortcut_keys))
prof.register_ac(" ", shortcut_keys)

313
src/sounds.py Normal file
View File

@@ -0,0 +1,313 @@
"""
Play sounds on receiving messages.
Default settings require mpg123 to be installed on the host system:
sudo apt-get install mpg123
brew install mpg123
"""
from functools import lru_cache
from pathlib import Path
from typing import Tuple
import prof
import subprocess
import time
_last_played = {}
@lru_cache(maxsize=16)
def get_expanded_path(path_str: str) -> Tuple[Path, str]:
path = Path(path_str).expanduser()
return path, str(path)
def _play_sound_file(soundfile: str):
soundfile_path, soundfile_str = get_expanded_path(soundfile)
if not soundfile_path.exists():
prof.cons_show(f"Sound file not found: {soundfile_str}")
return
now = time.monotonic()
if _last_played.get(soundfile_str, 0) + 0.5 > now:
return # skip if played too recently
_last_played[soundfile_str] = now
audioplayer = prof.settings_string_get("sounds", "audioplayer", "mpg123 -q")
subprocess.Popen([*audioplayer.split(), soundfile_str], start_new_session=True)
def _play_sound(kind: str = "chat"):
soundfile = prof.settings_string_get("sounds", kind, None)
if soundfile:
_play_sound_file(soundfile)
def _cmd_sounds(arg1=None, arg2=None, arg3=None):
if not arg1:
prof.cons_show("")
enabled = prof.settings_boolean_get("sounds", "enabled", False)
chatsound = prof.settings_string_get("sounds", "chat", None)
roomsound = prof.settings_string_get("sounds", "room", None)
privatesound = prof.settings_string_get("sounds", "private", None)
roomlist = prof.settings_string_list_get("sounds", "rooms")
audioplayer = prof.settings_string_get("sounds", "audioplayer", "mpg123 -q")
if chatsound or roomsound or privatesound:
if enabled:
prof.cons_show("Sounds: ON")
else:
prof.cons_show("Sounds: OFF")
prof.cons_show("Audio Player: " + audioplayer)
if chatsound:
prof.cons_show(" Chat : " + chatsound)
if roomsound:
prof.cons_show(" Room : " + roomsound)
if roomlist and len(roomlist) > 0:
for room in roomlist:
prof.cons_show(" " + room)
else:
prof.cons_show(" All rooms")
if privatesound:
prof.cons_show(" Private : " + privatesound)
else:
prof.cons_show("No sounds set.")
return
if arg1 == "on":
prof.settings_boolean_set("sounds", "enabled", True)
prof.cons_show("Sounds enabled")
return
if arg1 == "off":
prof.settings_boolean_set("sounds", "enabled", False)
prof.cons_show("Sounds disabled")
return
if arg1 == "test":
if arg2 not in (
"chat",
"private",
"room",
):
prof.cons_bad_cmd_usage("/sounds")
return
enabled = prof.settings_boolean_get("sounds", "enabled", False)
if not enabled:
prof.cons_show("Sounds disabled")
return
soundfile = prof.settings_string_get("sounds", arg2, None)
if not soundfile:
prof.cons_show("No sound file for " + arg2)
return
_play_sound(kind=arg2)
return
if arg1 == "set":
if arg2 == "chat":
prof.settings_string_set("sounds", "chat", arg3)
prof.cons_show("Set chat sound: " + arg3)
elif arg2 == "room":
prof.settings_string_set("sounds", "room", arg3)
prof.cons_show("Set room sound: " + arg3)
elif arg2 == "private":
prof.settings_string_set("sounds", "private", arg3)
prof.cons_show("Set private sound: " + arg3)
elif arg2 == "audioplayer":
prof.settings_string_set("sounds", "audioplayer", arg3)
prof.cons_show("Set audio player: " + arg3)
else:
prof.cons_bad_cmd_usage("/sounds")
return
if arg1 == "reset":
if arg2 == "audioplayer":
prof.settings_string_set("sounds", "audioplayer", "mpg123 -q")
prof.cons_show("Reset audio player to default.")
else:
prof.cons_bad_cmd_usage("/sounds")
if arg1 == "clear":
if arg2 == "chat":
prof.settings_string_set("sounds", "chat", "")
prof.cons_show("Removed chat sound.")
elif arg2 == "room":
prof.settings_string_set("sounds", "room", "")
prof.cons_show("Removed room sound.")
elif arg2 == "private":
prof.settings_string_set("sounds", "private", "")
prof.cons_show("Removed private sound.")
else:
prof.cons_bad_cmd_usage("/sounds")
return
if arg1 == "rooms":
if arg2 == "add":
if not arg3:
prof.cons_bad_cmd_usage("/sounds")
else:
prof.settings_string_list_add("sounds", "rooms", arg3)
prof.cons_show("Sounds enabled for room: " + arg3)
elif arg2 == "remove":
if not arg3:
prof.cons_bad_cmd_usage("/sounds")
else:
prof.settings_string_list_remove("sounds", "rooms", arg3)
roomlist = prof.settings_string_list_get("sounds", "rooms")
if roomlist and len(roomlist) > 0:
prof.cons_show("Sounds disabled for room: " + arg3)
else:
prof.cons_show("Empty room list for sounds, playing in all rooms.")
elif arg2 == "clear":
prof.settings_string_list_clear("sounds", "rooms")
prof.cons_show("Cleared sounds room list, playing in all rooms.")
else:
prof.cons_bad_cmd_usage("/sounds")
return
if arg1 == "mute":
if not arg2:
prof.cons_bad_cmd_usage("/sounds mute <jid>")
else:
prof.settings_string_list_add("sounds", "muted", arg2)
prof.cons_show("Muted JID: " + arg2)
return
if arg1 == "unmute":
if not arg2:
prof.cons_bad_cmd_usage("/sounds unmute <jid>")
else:
prof.settings_string_list_remove("sounds", "muted", arg2)
prof.cons_show("Unmuted JID: " + arg2)
return
prof.cons_bad_cmd_usage("/sounds")
def prof_init(version, status, account_name, fulljid):
synopsis = [
"/sounds",
"/sounds on|off",
"/sounds set audioplayer <cmd>",
"/sounds reset audioplayer",
"/sounds set chat <file>",
"/sounds set room <file>",
"/sounds set private <file>",
"/sounds test private|room|chat",
"/sounds clear chat",
"/sounds clear room",
"/sounds clear private",
"/sounds rooms add <roomjid>",
]
description = "Play sounds on receiving messages. Executing the command without arguments displays current settings."
args = [
["on|off", "Enable or disable playing sounds."],
[
"set audioplayer <cmd>",
"Commmand line of audio player to use for playing sound files. Defaults to mpg123.",
],
["reset audioplayer", "Resets the audio player to the default mpg123."],
["set chat <file>", "Path to sound file to play on chat messages."],
["set room <file>", "Path to sound file to play on room messages."],
["set private <file>", "Path to sound file to play on private room messages."],
["test private|room|chat", "Test the configured audio player and sound file."],
["clear chat", "Remove the sound for chat messages."],
["clear room", "Remove the sound for room messages."],
["clear private", "Remove the sound for private messages."],
[
"rooms add <roomjid>",
"Add the room to the list that will play the room sound.",
],
[
"rooms remove <roomjid>",
"Remove the room from the list that will play the room sound.",
],
[
"rooms clear",
"Clear the room list, all rooms will play sounds on new messages.",
],
]
examples = [
"/sounds set audioplayer paplay",
"/sounds set chat ~/sounds/woof.mp3",
"/sounds set room ~/sounds/meow.mp3",
"/sounds set private ~/sounds/shhh.mp3",
"/sounds test private",
"/sounds remove private",
"/sounds rooms add myroom@conference.server.org",
"/sounds on",
]
prof.register_command(
"/sounds", 0, 3, synopsis, description, args, examples, _cmd_sounds
)
prof.completer_add(
"/sounds", ["set", "clear", "on", "off", "rooms", "reset", "mute", "unmute"]
)
prof.completer_add("/sounds set", ["chat", "room", "private", "audioplayer"])
prof.completer_add("/sounds reset", ["audioplayer"])
prof.completer_add("/sounds test", ["chat", "room", "private"])
prof.completer_add("/sounds clear", ["chat", "room", "private"])
prof.completer_add("/sounds rooms", ["add", "remove", "clear"])
prof.filepath_completer_add("/sounds set chat")
prof.filepath_completer_add("/sounds set room")
prof.filepath_completer_add("/sounds set private")
def prof_post_chat_message_display(barejid, resource, message):
enabled = prof.settings_boolean_get("sounds", "enabled", False)
if not enabled:
return
muted = prof.settings_string_list_get("sounds", "muted")
if muted and barejid in muted:
return
_play_sound(kind="chat")
def prof_post_room_message_display(barejid, nick, message):
my_nick = prof.get_room_nick(barejid)
if not my_nick:
return
if my_nick == nick:
return
enabled = prof.settings_boolean_get("sounds", "enabled", False)
if not enabled:
return
roomlist = prof.settings_string_list_get("sounds", "rooms")
if roomlist and (barejid not in roomlist):
return
muted = prof.settings_string_list_get("sounds", "muted")
if muted and barejid in muted:
return
_play_sound(soundfile, kind="room")
def prof_post_priv_message_display(barejid, nick, message):
enabled = prof.settings_boolean_get("sounds", "enabled", False)
if not enabled:
return
muted = prof.settings_string_list_get("sounds", "muted")
if muted and barejid in muted:
return
_play_sound(kind="private")

129
src/syscmd.py Normal file
View File

@@ -0,0 +1,129 @@
"""
Allow running system commands in a plugin window, or send result to recipient
Theme options in ~/.local/share/profanity/plugin_themes
[system]
command=magenta
result=green
"""
import prof
import subprocess
system_win = "System"
def _get_result(command):
return subprocess.Popen(command, shell=True, stdout=subprocess.PIPE).stdout.read()
def _handle_win_input(win, command):
prof.win_show_themed(win, "system", "command", None, command)
prof.win_show(win, "")
result = _get_result(command)
split = result.splitlines()
for s in split:
prof.win_show_themed(win, "system", "result", None, s)
prof.win_show(win, "")
def create_win():
if prof.win_exists(system_win) == False:
prof.win_create(system_win, _handle_win_input)
def _handle_send(command=None):
if command == None:
prof.cons_bad_cmd_usage("/system")
return
room = prof.get_current_muc()
recipient = prof.get_current_recipient()
if room == None and recipient == None:
prof.cons_show("You must be in a chat or muc window to send a system command")
prof.cons_alert()
return
result = _get_result(command)
newline = prof.settings_boolean_get("system", "newline", True)
if len(result.splitlines()) > 1 and newline:
prof.send_line(u'\u000A' + result.decode('utf-8', errors='ignore'))
else:
prof.send_line(result)
def _handle_exec(command=None):
if command == None:
prof.cons_bad_cmd_usage("/system")
return;
create_win()
prof.win_focus(system_win)
_handle_win_input(system_win, command)
def _handle_newline(setting=None):
if not setting:
prof.cons_show("")
newline = prof.settings_boolean_get("system", "newline", True)
if newline:
prof.cons_show("syscmd.py newline: on")
else:
prof.cons_show("syscmd.py newline: off")
return
if setting == "on":
prof.settings_boolean_set("system", "newline", True)
prof.cons_show("syscmd.py newline enabled.")
return
if setting == "off":
prof.settings_boolean_set("system", "newline", False)
prof.cons_show("syscmd.py newline disabled.")
return
prof.cons_bad_cmd_usage("/paste")
def _cmd_system(arg1=None, arg2=None):
if not arg1:
create_win()
prof.win_focus(system_win)
return;
if arg1 == "newline":
_handle_newline(arg2)
return
if arg1 == "send":
_handle_send(arg2)
return
if arg1 == "exec":
_handle_exec(arg2)
return
prof.cons_bad_cmd_usage("/system")
def prof_init(version, status, account_name, fulljid):
synopsis = [
"/system",
"/system newline on|off",
"/system exec <comman>",
"/system send <command>"
]
description = "Run a system command, calling with no arguments will open or focus the system window."
args = [
[ "newline on|off", "Send newline before multiline command output, defaults to on" ],
[ "exec <command>", "Execute a command" ],
[ "send <command>", "Send the result of the command to the current recipient or room" ]
]
examples = [
"/system exec ls -l",
"/system send uname -a"
]
prof.register_command("/system", 0, 2, synopsis, description, args, examples, _cmd_system)
prof.completer_add("/system", [ "exec", "send", "newline" ])
prof.completer_add("/system newline", [ "on", "off" ])

View File

@@ -1,79 +0,0 @@
"""
Allow running system commands in a plugin window, or send result to recipient
Theme options in ~/.local/share/profanity/plugin_themes
[system]
command=magenta
result=green
"""
import prof
import subprocess
system_win = "System"
def _get_result(command):
return subprocess.Popen(command, shell=True, stdout=subprocess.PIPE).stdout.read()
def _handle_win_input(win, command):
prof.win_show_themed(win, "system", "command", None, command)
prof.win_show(win, "")
result = _get_result(command)
split = result.splitlines()
for s in split:
prof.win_show_themed(win, "system", "result", None, s)
prof.win_show(win, "")
def create_win():
if prof.win_exists(system_win) == False:
prof.win_create(system_win, _handle_win_input)
def _cmd_system(arg1=None, arg2=None):
if not arg1:
create_win()
prof.win_focus(system_win)
elif arg1 == "send":
if arg2 == None:
prof.cons_bad_cmd_usage("/system")
else:
room = prof.get_current_muc()
recipient = prof.get_current_recipient()
if room == None and recipient == None:
prof.cons_show("You must be in a chat or muc window to send a system command")
prof.cons_alert()
else:
result = _get_result(arg2)
prof.send_line(u'\u000A' + result)
elif arg1 == "exec":
if arg2 == None:
prof.cons_bad_cmd_usage("/system")
else:
create_win()
prof.win_focus(system_win)
_handle_win_input(system_win, arg2)
else:
prof.cons_bad_cmd_usage("/system")
def prof_init(version, status, account_name, fulljid):
synopsis = [
"/system",
"/system exec <comman>",
"/system send <command>"
]
description = "Run a system command, calling with no arguments will open or focus the system window."
args = [
[ "exec <command>", "Execute a command" ],
[ "send <command>", "Send the result of the command to the current recipient or room" ]
]
examples = [
"/system exec ls -l",
"/system send uname -a"
]
prof.register_command("/system", 0, 2, synopsis, description, args, examples, _cmd_system)
prof.completer_add("/system", [ "exec", "send" ])

View File

@@ -1,161 +0,0 @@
module RubyTest
def self.prof_init(version, status)
Prof::cons_show("RubyTest: init, " + version + ", " + status)
Prof::register_command("/ruby", 0, 1, "/ruby [arg]", "RubyTest", "RubyTest", cmd_ruby)
Prof::register_command("/rb_upper", 1, 1, "/rb_upper string", "RubyTest", "RubyTest", cmd_upper)
Prof::register_command("/rb_notify", 0, 0, "/rb_notify", "RubyTest", "RubyTest", cmd_notify)
Prof::register_command("/rb_vercheck", 0, 0, "/rb_vercheck", "RubyTest", "RubyTest", cmd_vercheck)
Prof::register_ac("/rb_complete", [ "aaaa", "bbbb", "bcbcbc" ])
Prof::register_ac("/rb_complete aaaa", [ "one", "two", "three", "four" ])
Prof::register_ac("/rb_complete bcbcbc", [ "james", "jim", "jane", "bob" ])
Prof::register_command("/rb_complete", 0, 2, "/rb_complete [arg1] [arg2]", "RubyTest", "RubyTest", cmd_ac)
Prof::register_timed(timer_test, 30)
end
def self.prof_on_start
Prof::cons_show("RubyTest: prof_on_start")
Prof::log_debug("RubyTest: logged debug");
Prof::log_info("RubyTest: logged info");
Prof::log_warning("RubyTest: logged warning");
Prof::log_error("RubyTest: logged error");
end
def self.prof_on_shutdown
Prof::log_info("RubyTest: prof_on_shutdown");
end
def self.prof_on_connect(account_name, fulljid)
Prof::cons_show("RubyTest: prof_on_connect, " + account_name + ", " + fulljid)
end
def self.prof_on_disconnect(account_name, fulljid)
Prof::cons_show("RubyTest: prof_on_disconnect, " + account_name + ", " + fulljid)
end
def self.prof_pre_chat_message_display(jid, message)
Prof::cons_show("RubyTest: prof_pre_chat_message_display, " + jid + ", " + message)
Prof::cons_alert
return message + "[RB_pre_chat_message_display]"
end
def self.prof_post_chat_message_display(jid, message)
Prof::cons_show("RubyTest: prof_post_chat_message_display, " + jid + ", " + message)
Prof::cons_alert
end
def self.prof_pre_chat_message_send(jid, message)
Prof::cons_show("RubyTest: prof_pre_chat_message_send, " + jid + ", " + message)
Prof::cons_alert
return message + "[RB_pre_chat_message_send]"
end
def self.prof_post_chat_message_send(jid, message)
Prof::cons_show("RubyTest: prof_post_chat_message_send, " + jid + ", " + message)
Prof::cons_alert
end
def self.prof_pre_room_message_display(room, nick, message)
Prof::cons_show("RubyTest: prof_pre_room_message_display, " + room + ", " + nick + ", " + message)
Prof::cons_alert
return message + "[RB_pre_room_message_display]"
end
def self.prof_post_room_message_display(room, nick, message)
Prof::cons_show("RubyTest: prof_post_room_message_display, " + room + ", " + nick + ", " + message)
Prof::cons_alert
end
def self.prof_pre_room_message_send(room, message)
Prof::cons_show("RubyTest: prof_pre_room_message_send, " + room + ", " + message)
Prof::cons_alert
return message + "[RB_pre_room_message_send]"
end
def self.prof_post_room_message_send(room, message)
Prof::cons_show("RubyTest: prof_post_room_message_send, " + room + ", " + message)
Prof::cons_alert
end
def self.prof_pre_priv_message_display(room, nick, message)
Prof::cons_show("RubyTest: prof_pre_priv_message_display, " + room + ", " + nick + ", " + message)
Prof::cons_alert
return message + "[RB_pre_priv_message_display]"
end
def self.prof_post_priv_message_display(room, nick, message)
Prof::cons_show("RubyTest: prof_post_priv_message_display, " + room + ", " + nick + ", " + message)
Prof::cons_alert
end
def self.prof_pre_priv_message_send(room, nick, message)
Prof::cons_show("RubyTest: prof_pre_priv_message_send, " + room + ", " + nick + ", " + message)
Prof::cons_alert
return message + "[RB_pre_priv_message_send]"
end
def self.prof_post_priv_message_send(room, nick, message)
Prof::cons_show("RubyTest: prof_post_priv_message_send, " + room + ", " + nick + ", " + message)
Prof::cons_alert
end
def self.cmd_ruby
return Proc.new { | msg |
if msg
Prof::cons_show("RubyTest: /ruby command called, arg = " + msg)
else
Prof::cons_show("RubyTest: /ruby command called with no arg")
end
}
end
def self.cmd_ac
return Proc.new { | arg1, arg2 |
Prof::cons_show("RubyTest: /rb_complete called")
}
end
def self.cmd_notify
return Proc.new {
Prof::notify("RubyTest: notify", 2000, "Plugins")
}
end
def self.cmd_vercheck
return Proc.new {
Prof::send_line("/vercheck")
Prof::cons_show("RubyTest: sent \"/vercheck\" command")
}
end
def self.timer_test
return Proc.new {
Prof::cons_show("RubyTest: timer fired.")
Prof::cons_alert
}
end
def self.cmd_upper
return Proc.new { | line |
win_tag = "Ruby Plygin"
if (Prof::win_exists(win_tag) == false)
Prof::win_create(win_tag, handle_upper)
end
Prof::win_focus(win_tag)
if (line)
handle_upper.call(win_tag, line)
end
}
end
def self.handle_upper
return Proc.new { | win, line |
upper = line.upcase
Prof::win_show(win, upper)
Prof::win_show_red(win, upper + " red")
Prof::win_show_yellow(win, upper + " yellow")
Prof::win_show_green(win, upper + " green")
Prof::win_show_cyan(win, upper + " cyan")
}
end
end

View File

@@ -1,23 +0,0 @@
module RubyThread
@counter = 0
def self.inc_counter()
while true
sleep(1)
@counter = @counter + 1
end
end
def self.cmd_rcount()
return Proc.new {
instance_eval {
Prof::cons_show("RCounter: " + @counter)
}
}
end
def self.prof_init(version, status)
Thread.new { self.inc_counter() }
Prof::register_command("/rcount", 0, 0, "/rcount", "Ruby threaded example", "Ruby threaded example", cmd_rcount)
end
end

View File

@@ -1,116 +0,0 @@
local luatest = {}
local function cmd_lua(msg)
if msg then
prof_cons_show("luatest: /lua command called, arg = " .. msg)
else
prof_cons_show("luatest: /lua command called with no arg")
end
prof_cons_alert()
prof_notify("luatest: notify", 8000, "Plugins")
prof_send_line("/account list")
prof_cons_show("luatest: sent \"/account list\" command")
end
local function timer_test()
prof_cons_show("luatest: timer fired.")
recipient = prof_get_current_recipient()
if recipient then
prof_cons_show(" current recipient = " .. recipient)
end
prof_cons_alert()
end
local function handle_bracket(win, line)
prof_win_show(win, "(" .. line .. ")")
prof_win_show_red(win, "Red")
prof_win_show_yellow(win, "Yellow")
prof_win_show_green(win, "Green")
prof_win_show_cyan(win, "Cyan")
end
local function cmd_bracket(line)
win_tag = "Parenthesise echo";
if prof_win_exists(win_tag) == false then
prof_win_create(win_tag, handle_bracket);
end
prof_win_focus(win_tag);
if line then
handle_bracket(win_tag, line)
end
end
function luatest.prof_init(version, status)
prof_cons_show("luatest: init, " .. version .. ", " .. status)
prof_register_command("/lua", 0, 1, "/lua", "luatest", "luatest", cmd_lua)
prof_register_command("/bracket", 0, 1, "/bracket", "Parenthesise input string", "Parenthesise input string", cmd_bracket);
prof_register_ac("/lcomplete", { "football", "footy", "tennis" })
prof_register_ac("/lcomplete football", { "goal", "out", "try", "given" })
prof_register_ac("/lcomplete tennis", { "test", "prod", "dev", "stage" })
prof_register_command("/lcomplete", 0, 2, "/lcomplete", "Lua completion", "Lua completion", cmd_bracket)
prof_register_timed(timer_test, 10)
end
function luatest.prof_on_start()
prof_cons_show("luatest: on_start")
prof_log_debug("luatest: logged debug");
prof_log_info("luatest: logged info");
prof_log_warning("luatest: logged warning");
prof_log_error("luatest: logged error");
end
function luatest.prof_on_connect(account_name, fulljid)
prof_cons_show("luatest: on_connect, " .. account_name .. ", " .. fulljid)
end
function luatest.prof_on_disconnect(account_name, fulljid)
prof_cons_show("luatest: on_disconnect, " .. account_name .. ", " .. fulljid)
prof_log_info("luatest: on_disconnect, " .. account_name .. ", " .. fulljid)
end
function luatest.prof_on_message_received(jid, message)
prof_cons_show("luatest: on_message_received, " .. jid .. ", " .. message)
prof_cons_alert()
return message .. "[LUA]"
end
function luatest.prof_on_room_message_received(room, nick, message)
prof_cons_show("luatest: on_room_message_received, " .. room .. ", " .. nick .. ", " .. message)
prof_cons_alert()
return message .. "[LUA]"
end
function luatest.prof_on_private_message_received(room, nick, message)
prof_cons_show("luatest: on_private_message_received, " .. room .. ", " .. nick .. ", " .. message)
prof_cons_alert()
return message .. "[LUA]"
end
function luatest.prof_on_message_send(jid, message)
prof_cons_show("luatest: on_message_send, " .. jid .. ", " .. message)
prof_cons_alert()
return message .. "[LUA]"
end
function luatest.prof_on_private_message_send(room, nick, message)
prof_cons_show("luatest: on_private_message_send, " .. room .. ", " .. nick .. ", " .. message)
prof_cons_alert()
return message .. "[LUA]"
end
function luatest.prof_on_room_message_send(room, message)
prof_cons_show("luatest: on_room_message_send, " .. room .. ", " .. message)
prof_cons_alert()
return message .. "[LUA]"
end
function luatest.prof_on_shutdown()
prof_log_info("luatest: on_shutdown")
end
return luatest

View File

@@ -1,692 +0,0 @@
import prof
import threading
import time
plugin_win = "Python Test"
count = 0
ping_id = 1
count_thread = None
thread_stop = None
def _inc_counter():
global count
global thread_stop
while not thread_stop.is_set():
time.sleep(5)
count = count + 1
def _handle_win_input(win, line):
prof.win_show(win, "Input received: " + line)
def _consalert():
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.cons_alert()
prof.win_show(plugin_win, "called -> prof.cons_alert")
def _consshow(msg):
if not msg:
prof.cons_bad_cmd_usage("/python-test")
return
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.cons_show(msg)
prof.win_show(plugin_win, "called -> prof.cons_show: " + msg)
def _consshow_t(group, key, dflt, msg):
if not group or not key or not dflt or not msg:
prof.cons_bad_cmd_usage("/python-test")
return
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
groupval = None if group == "none" else group
keyval = None if key == "none" else key
dfltval = None if dflt == "none" else dflt
prof.cons_show_themed(groupval, keyval, dfltval, msg)
prof.win_show(plugin_win, "called -> prof.cons_show_themed: " + group + ", " + key + ", " + dflt + ", " + msg)
def _constest():
res = prof.current_win_is_console()
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
if res:
prof.win_show(plugin_win, "called -> prof.current_win_is_console: true")
else:
prof.win_show(plugin_win, "called -> prof.current_win_is_console: false")
def _winshow(msg):
if not msg:
prof.cons_bad_cmd_usage("/python-test")
return
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.win_show(plugin_win, msg)
prof.win_show(plugin_win, "called -> prof.win_show: " + msg)
def _winshow_t(group, key, dflt, msg):
if not group or not key or not dflt or not msg:
prof.cons_bad_cmd_usage("/python-test")
return
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
groupval = None if group == "none" else group
keyval = None if key == "none" else key
dfltval = None if dflt == "none" else dflt
prof.win_show_themed(plugin_win, groupval, keyval, dfltval, msg)
prof.win_show(plugin_win, "called -> prof_win_show_themed: " + group + ", " + key + ", " + dflt + ", " + msg)
def _sendline(line):
if not line:
prof.cons_bad_cmd_usage("/python-test")
return
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.send_line(line)
prof.win_show(plugin_win, "called -> prof.send_line: " + line)
def _notify(msg):
if not msg:
prof.cons_bad_cmd_usage("/python-test")
return
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.notify(msg, 5000, "python-test plugin")
prof.win_show(plugin_win, "called -> prof.notify: " + msg)
def _get(subject):
if subject == "recipient":
prof.win_create(plugin_win, _handle_win_input)
recipient = prof.get_current_recipient();
if recipient:
prof.win_focus(plugin_win)
prof.win_show(plugin_win, "called -> prof.get_current_recipient: " + recipient)
else:
prof.win_focus(plugin_win)
prof.win_show(plugin_win, "called -> prof_get_current_recipient: <none>")
elif subject == "room":
prof.win_create(plugin_win, _handle_win_input)
room = prof.get_current_muc()
if room:
prof.win_focus(plugin_win)
prof.win_show(plugin_win, "called -> prof_get_current_muc: " + room)
else:
prof.win_focus(plugin_win)
prof.win_show(plugin_win, "called -> prof_get_current_muc: <none>")
elif subject == "nick":
prof.win_create(plugin_win, _handle_win_input)
nick = prof.get_current_nick()
if nick:
prof.win_focus(plugin_win)
prof.win_show(plugin_win, "called -> prof_get_current_nick: " + nick)
else:
prof.win_focus(plugin_win)
prof.win_show(plugin_win, "called -> prof_get_current_nick: <none>")
elif subject == "occupants":
prof.win_create(plugin_win, _handle_win_input)
occupants = prof.get_current_occupants()
if occupants:
prof.win_focus(plugin_win)
prof.win_show(plugin_win, "called -> prof_get_current_occupants:")
for occupant in occupants:
prof.win_show(plugin_win, occupant)
else:
prof.win_focus(plugin_win)
prof.win_show(plugin_win, "called -> prof_get_current_occupants: <none>")
else:
prof.cons_bad_cmd_usage("/python-test")
def _log(level, msg):
if not level or not msg:
prof.cons_bad_cmd_usage("/python-test")
return
if level == "debug":
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.log_debug(msg)
prof.win_show(plugin_win, "called -> prof.log_debug: " + msg)
elif level == "info":
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.log_info(msg)
prof.win_show(plugin_win, "called -> prof.log_info: " + msg)
elif level == "warning":
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.log_warning(msg)
prof.win_show(plugin_win, "called -> prof.log_warning: " + msg)
elif level == "error":
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.log_error(msg)
prof.win_show(plugin_win, "called -> prof.log_error: " + msg)
else:
prof.cons_bad_cmd_usage("/python-test")
def _count():
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.win_show(plugin_win, "Count: " + str(count))
def _ping(jid):
global ping_id
if not jid:
prof.cons_bad_cmd_usage("/python-test")
return
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
res = prof.send_stanza("<iq to='" + jid + "' id='pythonplugin-" + str(ping_id) + "' type='get'><ping xmlns='urn:xmpp:ping'/></iq>")
ping_id = ping_id + 1
if res:
prof.win_show(plugin_win, "Ping sent successfully")
else:
prof.win_show(plugin_win, "Error sending ping")
def _boolean(op, group, key, value_str):
if op != "get" and op != "set":
prof.cons_bad_cmd_usage("/python-test")
return
if group == None or key == None:
prof.cons_bad_cmd_usage("/python-test")
return
if op == "set" and value_str != "true" and value_str != "false":
prof.cons_bad_cmd_usage("/python-test")
return
if op == "get":
dflt = False
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
res = prof.settings_boolean_get(group, key, dflt)
if res:
prof.win_show(plugin_win, "Boolean setting: TRUE")
else:
prof.win_show(plugin_win, "Boolean setting: FALSE")
elif op == "set":
value = False
if value_str == "true":
value = True
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.settings_boolean_set(group, key, value)
prof.win_show(plugin_win, "Set [" + group + "] " + key + " to " + str(value))
def _string(op, group, key, value):
if op != "get" and op != "set":
prof.cons_bad_cmd_usage("/python-test")
return
if group == None or key == None:
prof.cons_bad_cmd_usage("/python-test")
return
if op == "set" and not value:
prof.cons_bad_cmd_usage("/python-test")
return
if op == "get":
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
res = prof.settings_string_get(group, key, None)
if res:
prof.win_show(plugin_win, "String setting: " + res)
else:
prof.win_show(plugin_win, "String setting: None")
elif op == "set":
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.settings_string_set(group, key, value)
prof.win_show(plugin_win, "Set [" + group + "] " + key + " to " + value)
def _string_list(op, group, key, value):
if op != "get" and op != "add" and op !="remove" and op != "remove_all":
prof.cons_bad_cmd_usage("/python-test")
return
if op == "get":
if group == None or key == None:
prof.cons_bad_cmd_usage("/python-test")
return
res = prof.settings_string_list_get(group, key)
prof.win_focus(plugin_win)
if res is None:
prof.win_show(plugin_win, "No list found")
return
prof.win_show(plugin_win, "String list:")
for el in res:
prof.win_show(plugin_win, " " + el)
return
if op == "add":
if group == None or key == None or value == None:
prof.cons_bad_cmd_usage("/python-test")
return
prof.settings_string_list_add(group, key, value)
prof.win_focus(plugin_win)
prof.win_show(plugin_win, "Added '" + value + "' to [" + group + "]" + " " + key)
return
if op == "remove":
if group == None or key == None or value == None:
prof.cons_bad_cmd_usage("/python-test")
return
res = prof.settings_string_list_remove(group, key, value)
prof.win_focus(plugin_win)
if res:
prof.win_show(plugin_win, "Removed '" + value + "' from [" + group + "]" + " " + key)
else:
prof.win_show(plugin_win, "Error removing string item from list")
return;
if op == "remove_all":
if group == None or key == None:
prof.cons_bad_cmd_usage("/python-test")
return
res = prof.settings_string_list_clear(group, key)
prof.win_focus(plugin_win)
if res:
prof.win_show(plugin_win, "Removed all items from [" + group + "]" + " " + key)
else:
prof.win_show(plugin_win, "Error removing list")
return
def _int(op, group, key, value):
if op != "get" and op != "set":
prof.cons_bad_cmd_usage("/python-test")
return
if group == None or key == None:
prof.cons_bad_cmd_usage("/python-test")
return
if op == "get":
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
res = prof.settings_int_get(group, key, 0)
prof.win_show(plugin_win, "Integer setting: " + str(res))
elif op == "set":
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.settings_int_set(group, key, int(value))
prof.win_show(plugin_win, "Set [" + group + "] " + key + " to " + str(value))
def _incoming(barejid, resource, message):
if not barejid or not resource or not message:
prof.cons_bad_cmd_usage("/python-test")
return
prof.incoming_message(barejid, resource, message)
def _completer(op, item):
if not item:
prof.cons_bad_cmd_usage("/python-test")
return
if op == "add":
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.completer_add("/python-test", [item])
prof.win_show(plugin_win, "Added \"" + item + "\" to /python-test completer")
prof.completer_add("/python-test completer remove", [item])
elif op == "remove":
prof.win_create(plugin_win, _handle_win_input)
prof.win_focus(plugin_win)
prof.completer_remove("/python-test", [item])
prof.win_show(plugin_win, "Removed \"" + item + "\" to /python-test completer")
prof.completer_remove("/python-test completer remove", [item])
else:
prof.cons_bad_cmd_usage("/python-test")
def _cmd_pythontest(subcmd=None, arg1=None, arg2=None, arg3=None, arg4=None):
if subcmd == "consalert": _consalert()
elif subcmd == "consshow": _consshow(arg1)
elif subcmd == "consshow_t": _consshow_t(arg1, arg2, arg3, arg4)
elif subcmd == "constest": _constest()
elif subcmd == "winshow": _winshow(arg1)
elif subcmd == "winshow_t": _winshow_t(arg1, arg2, arg3, arg4)
elif subcmd == "sendline": _sendline(arg1)
elif subcmd == "notify": _notify(arg1)
elif subcmd == "get": _get(arg1)
elif subcmd == "log": _log(arg1, arg2)
elif subcmd == "count": _count()
elif subcmd == "ping": _ping(arg1)
elif subcmd == "boolean": _boolean(arg1, arg2, arg3, arg4)
elif subcmd == "string": _string(arg1, arg2, arg3, arg4)
elif subcmd == "string_list": _string_list(arg1, arg2, arg3, arg4)
elif subcmd == "int": _int(arg1, arg2, arg3, arg4)
elif subcmd == "incoming": _incoming(arg1, arg2, arg3)
elif subcmd == "completer": _completer(arg1, arg2)
else: prof.cons_bad_cmd_usage("/python-test")
def timed_callback():
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "timed -> timed_callback called")
def prof_init(version, status, account_name, fulljid):
global count
global ping_id
global thread_stop
count = 0
ping_id = 1
thread_stop = threading.Event()
count_thread = threading.Thread(target=_inc_counter)
count_thread.daemon = True
count_thread.start()
prof.disco_add_feature("urn:xmpp:profanity:python_test_plugin");
prof.win_create(plugin_win, _handle_win_input)
if account_name and fulljid:
prof.win_show(plugin_win, "fired -> prof_init: " + version + ", " + status + ", " + account_name + ", " + fulljid)
else:
prof.win_show(plugin_win, "fired -> prof_init: " + version + ", " + status)
synopsis = [
"/python-test consalert",
"/python-test consshow <message>",
"/python-test consshow_t <group> <key> <default> <message>",
"/python-test constest",
"/python-test winshow <message>",
"/python-test winshow_t <group> <key> <default> <message>",
"/python-test notify <message>",
"/python-test sendline <line>",
"/python-test get recipient|room|nick|occupants",
"/python-test log debug|info|warning|error <message>",
"/python-test count",
"/python-test ping <jid>",
"/python-test boolean get <group> <key>",
"/python-test boolean set <group> <key> <value>",
"/python-test string get <group> <key>",
"/python-test string set <group> <key> <value>",
"/python-test string_list get <group> <key>",
"/python-test string_list add <group> <key> <value>",
"/python-test string_list remove <group> <key> <value>",
"/python-test string_list remove_all <group> <key>",
"/python-test int get <group> <key>",
"/python-test int set <group> <key> <value>",
"/python-test incoming <barejid> <resource> <message>",
"/python-test completer add|remove <item>"
]
description = "Python test plugins. All commands focus the plugin window."
args = [
[ "consalert", "Highlight the console window in the status bar" ],
[ "consshow <message>", "Show the message in the console window" ],
[ "consshow_t <group> <key> <default> <message>", "Show the themed message in the console window. " ],
[ "constest", "Show whether the command was run in the console." ],
[ "winshow <message>", "Show the message in the plugin window" ],
[ "winshow_t <group> <key> <default> <message>", "Show the themed message in the plugin window. " ],
[ "notify <message>", "Send a desktop notification with message" ],
[ "sendline <line>", "Pass line to profanity to process" ],
[ "get recipient", "Show the current chat recipient, if in a chat window" ],
[ "get room", "Show the current room JID, if ina a chat room"],
[ "get nick", "Show nickname in current room, if ina a chat room"],
[ "get occupants", "Show occupants in current room, if ina a chat room"],
[ "log debug|info|warning|error <message>", "Log a message at the specified level" ],
[ "count", "Show the counter, incremented every 5 seconds by a worker thread" ],
[ "ping <jid>", "Send an XMPP ping to the specified Jabber ID" ],
[ "boolean get <group> <key>", "Get a boolean setting" ],
[ "boolean set <group> <key> <value>", "Set a boolean setting" ],
[ "string get <group> <key>", "Get a string setting" ],
[ "string set <group> <key> <value>", "Set a string setting" ],
[ "string_list get <group> <key>", "Get a string list setting" ],
[ "string_list add <group> <key> <value>", "Add a string to a string list setting" ],
[ "string_list remove <group> <key> <value>", "Remove a string from a string list setting" ],
[ "string_list remove_all <group> <key>", "Remove all strings from a string list setting" ],
[ "int get <group> <key>", "Get a integer setting" ],
[ "int set <group> <key> <value>", "Set a integer setting" ],
[ "incoming <barejid> <resource> <message>", "Show an incoming message." ],
[ "completer add <item>", "Add an autocomplete item to the /c-test command." ],
[ "completer remove <item>", "Remove an autocomplete item from the /c-test command." ]
]
examples = [
"/python-test sendline /about",
"/python-test log debug \"Test debug message\"",
"/python-test consshow_t c-test cons.show none \"This is themed\"",
"/python-test consshow_t none none bold_cyan \"This is bold_cyan\"",
"/python-test ping buddy@server.org"
]
prof.register_command("/python-test", 1, 5, synopsis, description, args, examples, _cmd_pythontest)
prof.completer_add("/python-test",
[
"consalert",
"consshow",
"consshow_t",
"constest",
"winshow",
"winshow_t",
"notify",
"sendline",
"get",
"log",
"count",
"ping",
"boolean",
"string",
"string_list",
"int",
"incoming",
"completer"
]
)
prof.completer_add("/python-test get",
[ "recipient", "room", "nick", "occupants" ]
)
prof.completer_add("/python-test log",
[ "debug", "info", "warning", "error" ]
)
prof.completer_add("/python-test boolean",
[ "get", "set" ]
)
prof.completer_add("/python-test string",
[ "get", "set" ]
)
prof.completer_add("/python-test string_list",
[ "get", "add", "remove", "remove_all" ]
)
prof.completer_add("/python-test int",
[ "get", "set" ]
)
prof.completer_add("/python-test completer",
[ "add", "remove" ]
)
prof.register_timed(timed_callback, 5)
def prof_on_start():
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_on_start")
def prof_on_shutdown():
global thread_stop
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_on_shutdown")
thread_stop.set()
count_thread.join()
def prof_on_unload():
global thread_stop
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_on_unload")
thread_stop.set()
count_thread.join()
def prof_on_connect(account_name, fulljid):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_on_connect: " + account_name + ", " + fulljid)
def prof_on_disconnect(account_name, fulljid):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_on_disconnect: " + account_name + ", " + fulljid)
def prof_pre_chat_message_display(barejid, resource, message):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_pre_chat_message_display: " + barejid + "/" + resource + ", " + message)
def prof_post_chat_message_display(barejid, resource, message):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_post_chat_message_display: " + barejid + "/" + resource + ", " + message)
def prof_pre_chat_message_send(barejid, message):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_pre_chat_message_send: " + barejid + ", " + message)
def prof_post_chat_message_send(barejid, message):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_post_chat_message_send: " + barejid + ", " + message)
def prof_pre_room_message_display(barejid, nick, message):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_pre_room_message_display: " + barejid + ", " + nick + ", " + message)
def prof_post_room_message_display(barejid, nick, message):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_post_room_message_display: " + barejid + ", " + nick + ", " + message)
def prof_pre_room_message_send(barejid, message):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_pre_room_message_send: " + barejid + ", " + message)
def prof_post_room_message_send(barejid, message):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_post_room_message_send: " + barejid + ", " + message)
def prof_on_room_history_message(barejid, nick, message, timestamp):
prof.win_create(plugin_win, _handle_win_input)
if timestamp:
prof.win_show(plugin_win, "fired -> prof_on_room_history_message: " + barejid + ", " + nick + ", " + message + ", " + timestamp)
else:
prof.win_show(plugin_win, "fired -> prof_on_room_history_message: " + barejid + ", " + nick + ", " + message)
def prof_pre_priv_message_display(barejid, nick, message):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_pre_priv_message_display: " + barejid + ", " + nick + ", " + message)
def prof_post_priv_message_display(barejid, nick, message):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_post_priv_message_display: " + barejid + ", " + nick + ", " + message)
def prof_pre_priv_message_send(barejid, nick, message):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_pre_priv_message_send: " + barejid + ", " + nick + ", " + message)
def prof_post_priv_message_send(barejid, nick, message):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_post_priv_message_send: " + barejid + ", " + nick + ", " + message)
def prof_on_message_stanza_send(stanza):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_on_message_stanza_send: " + stanza)
def prof_on_message_stanza_receive(stanza):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_on_message_stanza_receive: " + stanza)
return True
def prof_on_presence_stanza_send(stanza):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_on_presence_stanza_send: " + stanza)
def prof_on_presence_stanza_receive(stanza):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_on_presence_stanza_receive: " + stanza)
return True
def prof_on_iq_stanza_send(stanza):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_on_iq_stanza_send: " + stanza)
def prof_on_iq_stanza_receive(stanza):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_on_iq_stanza_receive: " + stanza)
return True
def prof_on_contact_offline(barejid, resource, status):
prof.win_create(plugin_win, _handle_win_input)
if status:
prof.win_show(plugin_win, "fired -> prof_on_contact_offline: " + barejid + "/" + resource + " \"" + status + "\"")
else:
prof.win_show(plugin_win, "fired -> prof_on_contact_offline: " + barejid + "/" + resource)
def prof_on_contact_presence(barejid, resource, presence, status, priority):
prof.win_create(plugin_win, _handle_win_input)
if status:
prof.win_show(plugin_win, "fired -> prof_on_contact_presence: " + barejid + "/" + resource + " " + presence + " " + str(priority) + " \"" + status + "\"")
else:
prof.win_show(plugin_win, "fired -> prof_on_contact_presence: " + barejid + "/" + resource + " " + presence + " " + str(priority))
def prof_on_chat_win_focus(barejid):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_on_chat_win_focus: " + barejid)
def prof_on_room_win_focus(barejid):
prof.win_create(plugin_win, _handle_win_input)
prof.win_show(plugin_win, "fired -> prof_on_room_win_focus: " + barejid)

View File

@@ -1,18 +0,0 @@
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
LINK_FLAGS := -fno-common -flat_namespace -bundle -undefined suppress
else
LINK_FLAGS := -shared -fpic
endif
all: test-c-plugin.so
%.so:%.o
$(CC) $(LINK_FLAGS) -lprofanity -o $@ $^
%.o:%.c
$(CC) $(INCLUDE) -D_GNU_SOURCE -D_BSD_SOURCE -fpic -g3 -O0 -std=c99 \
-pedantic -c -o $@ $<
clean:
$(RM) test-c-plugin.so

File diff suppressed because it is too large Load Diff