├── .editorconfig
├── .gitignore
├── .gitmodules
├── .luacheckrc
├── Dockerfile
├── LICENSE
├── README.md
├── config.fnl
├── extern
├── fennel
├── fennel.lua
└── fennelview.fnl
├── fennel_preamble.lua
├── install-dependencies.sh
├── launch.sh
├── main.lua
├── otouto
├── autils.lua
├── bot.lua
├── plugins
│ ├── admin
│ │ ├── add_admin.lua
│ │ ├── add_group.lua
│ │ ├── add_mod.lua
│ │ ├── antibot.lua
│ │ ├── antihammer_whitelist.lua
│ │ ├── antilink.lua
│ │ ├── antisquig.lua
│ │ ├── antisquigpp.lua
│ │ ├── antisticker.lua
│ │ ├── autopromoter.lua
│ │ ├── ban.lua
│ │ ├── ban_remover.lua
│ │ ├── deadmin.lua
│ │ ├── delete_join_messages.lua
│ │ ├── delete_left_messages.lua
│ │ ├── demod.lua
│ │ ├── files_only.lua
│ │ ├── filter.lua
│ │ ├── filterer.lua
│ │ ├── fix_perms.lua
│ │ ├── flags.lua
│ │ ├── get_description.lua
│ │ ├── get_link.lua
│ │ ├── hammer.lua
│ │ ├── interactive_flags.lua
│ │ ├── kick.lua
│ │ ├── kickme.lua
│ │ ├── list_admins.lua
│ │ ├── list_flags.fnl
│ │ ├── list_groups.lua
│ │ ├── list_mods.lua
│ │ ├── list_rules.lua
│ │ ├── mute.lua
│ │ ├── regen_link.lua
│ │ ├── remove_group.lua
│ │ ├── set_description.lua
│ │ ├── set_governor.lua
│ │ ├── set_rules.lua
│ │ ├── temp_ban.lua
│ │ ├── unhammer.lua
│ │ └── unrestrict.lua
│ ├── core
│ │ ├── about.fnl
│ │ ├── control.lua
│ │ ├── delete_messages.fnl
│ │ ├── disable_plugins.fnl
│ │ ├── end_forwards.fnl
│ │ ├── group_info.fnl
│ │ ├── group_whitelist.fnl
│ │ ├── help.lua
│ │ ├── luarun.fnl
│ │ ├── paged_lists.lua
│ │ ├── ping.fnl
│ │ ├── user_blacklist.fnl
│ │ ├── user_info.fnl
│ │ └── user_lists.fnl
│ └── user
│ │ ├── apod.fnl
│ │ ├── bible.fnl
│ │ ├── calc.fnl
│ │ ├── cat_fact.fnl
│ │ ├── cats.fnl
│ │ ├── commit.fnl
│ │ ├── currency.lua
│ │ ├── data
│ │ ├── eight_ball.fnl
│ │ └── slap.fnl
│ │ ├── dice.fnl
│ │ ├── dilbert.fnl
│ │ ├── dump.lua
│ │ ├── echo.fnl
│ │ ├── eight_ball.fnl
│ │ ├── full_width.lua
│ │ ├── google_translate.lua
│ │ ├── greetings.fnl
│ │ ├── hex_color.lua
│ │ ├── lastfm.lua
│ │ ├── maybe.fnl
│ │ ├── nickname.fnl
│ │ ├── reactions.fnl
│ │ ├── regex.fnl
│ │ ├── reminders.fnl
│ │ ├── shout.lua
│ │ ├── slap.fnl
│ │ ├── urban_dictionary.lua
│ │ ├── user_lookup.fnl
│ │ ├── whoami.fnl
│ │ ├── wikipedia.fnl
│ │ └── xkcd.fnl
├── rot13.fnl
└── utilities.lua
└── shell_preamble.fnl
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 |
7 | [*.lua]
8 | charset = utf-8
9 | indent_style = space
10 | indent_size = 4
11 |
12 | [*.fnl]
13 | charset = utf-8
14 | indent_style = space
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # https://github.com/topkecleon/otouto-plugins-topkecleon
2 | otouto/plugins/otouto-plugins-topkecleon
3 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "extern/anise"]
2 | path = extern/anise
3 | url = https://github.com/bb010g/anise.git
4 | [submodule "otouto/bindings"]
5 | path = extern/bindings
6 | url = https://github.com/topkecleon/bindings
7 |
--------------------------------------------------------------------------------
/.luacheckrc:
--------------------------------------------------------------------------------
1 | exclude_files = { "lume.lua" }
2 | ignore = { "_.*" }
3 | self = false
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Set OTOUTO_BOT_API_KEY to your telegram bot api key
2 | # Set OTOUTO_ADMIN_ID to your telegram id
3 | # Example: docker run -e OTOUTO_BOT_API_KEY="apikeyhere" -e OTOUTO_ADMIN_ID="idhere" jacobamason/otouto
4 | FROM alpine:3.7
5 |
6 | RUN apk --no-cache add --virtual build-deps \
7 | curl gcc libc-dev pcre-dev libressl-dev && \
8 | apk --no-cache add lua5.3 lua5.3-dev luarocks5.3 && \
9 | luarocks-5.3 install dkjson && \
10 | luarocks-5.3 install lpeg && \
11 | luarocks-5.3 install lrexlib-pcre && \
12 | luarocks-5.3 install luasec && \
13 | luarocks-5.3 install luasocket && \
14 | luarocks-5.3 install multipart-post && \
15 | luarocks-5.3 install serpent && \
16 | apk del build-deps
17 |
18 | COPY . /otouto
19 | WORKDIR /otouto
20 | CMD ./launch.sh
21 |
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | WIP, refer to the (outdated) readme in the main branch for now.
--------------------------------------------------------------------------------
/config.fnl:
--------------------------------------------------------------------------------
1 | (require-macros :anise.macros)
2 | (require* anise)
3 | ; For details on configuration values, see README.md#configuration.
4 | {
5 |
6 | ; Your authorization token from the botfather. (string, put quotes)
7 | :bot_api_key (os.getenv "OTOUTO_BOT_API_KEY")
8 | ; Your Telegram ID (number).
9 | :admin (math.floor (os.getenv "OTOUTO_ADMIN_ID"))
10 | ; Two-letter language code.
11 | ; Fetches it from the system if available, or defaults to English.
12 | :lang (let [lang (os.getenv "LANG")]
13 | (and-or lang (: lang :sub 1 2) "en"))
14 | ; The channel, group, or user to send error reports to.
15 | ; If this is not set, errors will be printed to the console.
16 | :log_chat (or (os.getenv "OTOUTO_LOG_ID") nil)
17 | ; The symbol that starts a command. Usually noted as "/" in documentation.
18 | :cmd_pat "/"
19 | ; The filename of the database. If left nil, defaults to $username.json.
20 | :database_name (os.getenv "OTOUTO_JSON_FILE")
21 | ; The block of text returned by /start and /about..
22 | :about_text
23 | "I am otouto, the plugin-wielding, multipurpose Telegram bot.\
24 | \
25 | Send /help to get started."
26 |
27 | ;; Third-party API keys
28 | ; The Cat API (thecatapi.com) (optional for cats)
29 | :cat_api_key nil
30 | ; Biblia (bibliaapi.com) (mandatory for bible)
31 | :biblia_api_key nil
32 | ; NASA APOD (api.nasa.gov) (optional for apod)
33 | :nasa_api_key "DEMO_KEY"
34 | ; Google (mandatory for google_translate)
35 | :google_api_key nil
36 | ; last.fm (mandatory for lastfm)
37 | :lastfm_api_key nil
38 |
39 | :paged_lists {
40 | :page_length 8
41 | :list_duration 900
42 | :private_lists false
43 | }
44 |
45 | ; some dumb stuff
46 | :user_lists {
47 | ; Lists are sorted alphabetically. Set to true to sort backward.
48 | :reverse_sort false
49 | }
50 |
51 | :user_info {
52 | ; If set to true, user info will only be collected in administrated groups.
53 | :admin_only false
54 | }
55 |
56 | ; Generic error messages.
57 | :errors {
58 | :generic "An unexpected error occurred."
59 | :connection "Connection error."
60 | :results "No results found."
61 | :argument "Invalid argument."
62 | :syntax "Invalid syntax."
63 | :specify_targets "Specify a target or targets by reply, username, or ID."
64 | :specify_target "Specify a target by reply, username, or ID."
65 | }
66 |
67 | :administration {
68 | ; Conversation, group, or channel for kick/ban notifications.
69 | ; Defaults to config.log_chat if left empty.
70 | :log_chat nil
71 | ; link or username
72 | :log_chat_username nil
73 | ; First strike warnings will be deleted after this, in seconds.
74 | :warning_expiration 30
75 | ; Default flag settings.
76 | :flags {
77 | :antibot true
78 | :antilink true
79 | }
80 | }
81 |
82 | :reactions {
83 | :shrug "¯\\_(ツ)_/¯"
84 | :lenny "( ͡° ͜ʖ ͡°)"
85 | :flip "(╯°□°)╯︵ ┻━┻"
86 | :look "ಠ_ಠ"
87 | :shots "SHOTS FIRED"
88 | :facepalm "(-‸ლ)"
89 | }
90 |
91 | :greetings {
92 | "Hello, #NAME." {
93 | "hello"
94 | "hey"
95 | "hi"
96 | "good morning"
97 | "good day"
98 | "good afternoon"
99 | "good evening"
100 | }
101 | "Goodbye, #NAME." {
102 | "good%-?bye"
103 | "bye"
104 | "later"
105 | "see ya"
106 | "good night"
107 | }
108 | "Welcome back, #NAME." {
109 | "i'm home"
110 | "i'm back"
111 | }
112 | "You're welcome, #NAME." {
113 | "thanks"
114 | "thank you"
115 | }
116 | }
117 |
118 | ; To enable a plugin, add its name to the list.
119 | :plugins (let
120 | [
121 | core-critical [
122 | :core.control
123 | :core.luarun
124 | :core.user_info
125 | :core.group_whitelist
126 | :core.group_info
127 | ]
128 | admin-critical [
129 | :admin.flags
130 | :admin.ban_remover
131 | :admin.autopromoter
132 | ]
133 | admin-filters [
134 | :admin.antibot
135 | :admin.antilink
136 | :admin.antisquigpp
137 | :admin.antisquig
138 | :admin.antisticker
139 | :admin.delete_left_messages
140 | :admin.delete_join_messages
141 | :admin.filterer
142 | :admin.files_only
143 | ]
144 | core [
145 | :core.end_forwards
146 | :core.user_blacklist
147 | :core.about
148 | :core.delete_messages
149 | :core.disable_plugins
150 | :core.help
151 | :core.paged_lists
152 | :core.ping
153 | :core.user_lists
154 | ]
155 | admin [
156 | :admin.add_admin
157 | :admin.add_group
158 | :admin.add_mod
159 | :admin.antihammer_whitelist
160 | :admin.ban
161 | :admin.deadmin
162 | :admin.demod
163 | :admin.filter
164 | :admin.fix_perms
165 | :admin.get_description
166 | :admin.get_link
167 | :admin.hammer
168 | :admin.interactive_flags
169 | :admin.kick
170 | :admin.kickme
171 | :admin.list_admins
172 | :admin.list_flags
173 | :admin.list_groups
174 | :admin.list_mods
175 | :admin.list_rules
176 | :admin.mute
177 | :admin.regen_link
178 | :admin.remove_group
179 | :admin.set_description
180 | :admin.set_governor
181 | :admin.set_rules
182 | :admin.temp_ban
183 | :admin.unhammer
184 | :admin.unrestrict
185 | ]
186 | user [
187 | :user.apod
188 | ;:user.bible
189 | :user.calc
190 | :user.cat_fact
191 | :user.cats
192 | :user.commit
193 | :user.currency
194 | :user.dice
195 | :user.dilbert
196 | :user.echo
197 | :user.eight_ball
198 | :user.full_width
199 | ;:user.google_translate
200 | :user.greetings
201 | :user.hex_color
202 | ;:user.lastfm
203 | :user.maybe
204 | :user.nickname
205 | :user.regex
206 | :user.reminders
207 | :user.slap
208 | :user.shout
209 | :user.urban_dictionary
210 | :user.user_lookup
211 | :user.whoami
212 | :user.wikipedia
213 | :user.xkcd
214 | :user.reactions
215 | ]
216 | ]
217 |
218 | (anise.concat
219 | core-critical
220 | ;admin-critical
221 | ;admin-filters
222 | core
223 | ;admin
224 | user
225 | ))
226 |
227 | }
228 |
--------------------------------------------------------------------------------
/extern/fennel:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env lua
2 |
3 | local fennel_dir = arg[0]:match("(.-)[^\\/]+$")
4 | package.path = fennel_dir .. "?.lua;" .. package.path
5 | local fennel = require('fennel')
6 |
7 | local help = [[
8 | Usage: fennel [FLAG] [FILE]
9 |
10 | --repl : Launch an interactive repl session
11 | --compile FILES : Compile files and write their Lua to stdout
12 | --help : Display this text
13 |
14 | When not given a flag, runs the file given as the first argument.]]
15 |
16 | local options = {
17 | sourcemap = true
18 | }
19 |
20 | local function dosafe(filename, opts, arg1)
21 | local ok, val = xpcall(function()
22 | return fennel.dofile(filename, opts, arg1)
23 | end, fennel.traceback)
24 | if not ok then
25 | print(val)
26 | os.exit(1)
27 | end
28 | return val
29 | end
30 |
31 | local compileOptHandlers = {
32 | ['--indent'] = function ()
33 | options.indent = table.remove(arg, 3)
34 | if options.indent == "false" then options.indent = false end
35 | table.remove(arg, 2)
36 | end,
37 | ['--sourcemap'] = function ()
38 | options.sourcemap = table.remove(arg, 3)
39 | if options.sourcemap == "false" then options.sourcemap = false end
40 | table.remove(arg, 2)
41 | end,
42 | }
43 |
44 | if arg[1] == "--repl" or #arg == 0 then
45 | local ppok, pp = pcall(fennel.dofile, fennel_dir .. "fennelview.fnl", options)
46 | if ppok then
47 | options.pp = pp
48 | end
49 | local initFilename = (os.getenv("HOME") or "") .. "/.fennelrc"
50 | local init = io.open(initFilename, "rb")
51 | if init then
52 | init:close()
53 | -- pass in options so fennerlrc can make changes to it
54 | dosafe(initFilename, options, options)
55 | end
56 | print("Welcome to fennel!")
57 | fennel.repl(options)
58 | elseif arg[1] == "--compile" then
59 | -- Handle options
60 | while compileOptHandlers[arg[2]] do
61 | compileOptHandlers[arg[2]]()
62 | end
63 | for i = 2, #arg do
64 | local f = assert(io.open(arg[i], "rb"))
65 | options.filename=arg[i]
66 | local ok, val = xpcall(function()
67 | return fennel.compileString(f:read("*all"), options)
68 | end, fennel.traceback)
69 | print(val)
70 | if not ok then os.exit(1) end
71 | f:close()
72 | end
73 | elseif #arg >= 1 and arg[1] ~= "--help" then
74 | local filename = table.remove(arg, 1) -- let the script have remaining args
75 | dosafe(filename)
76 | else
77 | print(help)
78 | end
79 |
--------------------------------------------------------------------------------
/extern/fennelview.fnl:
--------------------------------------------------------------------------------
1 | ;; A pretty-printer that outputs tables in Fennel syntax.
2 | ;; Loosely based on inspect.lua: http://github.com/kikito/inspect.lua
3 |
4 | (local quote (fn [str] (.. '"' (: str :gsub '"' '\\"') '"')))
5 |
6 | (local short-control-char-escapes
7 | {"\a" "\\a" "\b" "\\b" "\f" "\\f" "\n" "\\n"
8 | "\r" "\\r" "\t" "\\t" "\v" "\\v"})
9 |
10 | (local long-control-char-esapes
11 | (let [long {}]
12 | (for [i 0 31]
13 | (let [ch (string.char i)]
14 | (when (not (. short-control-char-escapes ch))
15 | (tset short-control-char-escapes ch (.. "\\" i))
16 | (tset long ch (: "\\%03d" :format i)))))
17 | long))
18 |
19 | (local escape (fn [str]
20 | (let [str (: str :gsub "\\" "\\\\")
21 | str (: str :gsub "(%c)%f[0-9]" long-control-char-esapes)]
22 | (: str :gsub "%c" short-control-char-escapes))))
23 |
24 | (local sequence-key? (fn [k len]
25 | (and (= (type k) "number")
26 | (<= 1 k)
27 | (<= k len)
28 | (= (math.floor k) k))))
29 |
30 | (local type-order {:number 1 :boolean 2 :string 3 :table 4
31 | :function 5 :userdata 6 :thread 7})
32 |
33 | (local sort-keys (fn [a b]
34 | (let [ta (type a) tb (type b)]
35 | (if (and (= ta tb) (~= ta "boolean")
36 | (or (= ta "string") (= ta "number")))
37 | (< a b)
38 | (let [dta (. type-order a)
39 | dtb (. type-order b)]
40 | (if (and dta dtb)
41 | (< dta dtb)
42 | dta true
43 | dtb false
44 | :else (< ta tb)))))))
45 |
46 | (local get-sequence-length
47 | (fn [t]
48 | (var len 1)
49 | (each [i (ipairs t)] (set len i))
50 | len))
51 |
52 | (local get-nonsequential-keys
53 | (fn [t]
54 | (let [keys {}
55 | sequence-length (get-sequence-length t)]
56 | (each [k (pairs t)]
57 | (when (not (sequence-key? k sequence-length))
58 | (table.insert keys k)))
59 | (table.sort keys sort-keys)
60 | (values keys sequence-length))))
61 |
62 | (local count-table-appearances
63 | (fn recur [t appearances]
64 | (if (= (type t) "table")
65 | (when (not (. appearances t))
66 | (tset appearances t 1)
67 | (each [k v (pairs t)]
68 | (recur k appearances)
69 | (recur v appearances)))
70 | (when (and t (= t t)) ; no nans please
71 | (tset appearances t (+ (or (. appearances t) 0) 1))))
72 | appearances))
73 |
74 |
75 |
76 | (var put-value nil) ; mutual recursion going on; defined below
77 |
78 | (local puts (fn [self ...]
79 | (each [_ v (ipairs [...])]
80 | (table.insert self.buffer v))))
81 |
82 | (local tabify (fn [self] (puts self "\n" (: self.indent :rep self.level))))
83 |
84 | (local already-visited? (fn [self v] (~= (. self.ids v) nil)))
85 |
86 | (local get-id (fn [self v]
87 | (var id (. self.ids v))
88 | (when (not id)
89 | (let [tv (type v)]
90 | (set id (+ (or (. self.max-ids tv) 0) 1))
91 | (tset self.max-ids tv id)
92 | (tset self.ids v id)))
93 | (tostring id)))
94 |
95 | (local put-sequential-table (fn [self t length]
96 | (puts self "[")
97 | (set self.level (+ self.level 1))
98 | (for [i 1 length]
99 | (puts self " ")
100 | (put-value self (. t i)))
101 | (set self.level (- self.level 1))
102 | (puts self " ]")))
103 |
104 | (local put-key (fn [self k]
105 | ;; TODO: :,bTrVyiC=z is not valid; comma cuts all off
106 | (if (and (= (type k) "string")
107 | (: k :find "^[%w?\\^_`!#$%&*+-./@~:|<=>]+$"))
108 | (puts self ":" k)
109 | (put-value self k))))
110 |
111 | (local put-kv-table (fn [self t]
112 | (puts self "{")
113 | (set self.level (+ self.level 1))
114 | (each [k v (pairs t)]
115 | (tabify self)
116 | (put-key self k)
117 | (puts self " ")
118 | (put-value self v))
119 | (set self.level (- self.level 1))
120 | (tabify self)
121 | (puts self "}")))
122 |
123 | (local put-table (fn [self t]
124 | (if (already-visited? self t)
125 | (puts self "#
")
126 | (>= self.level self.depth)
127 | (puts self "{...}")
128 | :else
129 | (let [(non-seq-keys length) (get-nonsequential-keys t)
130 | id (get-id self t)]
131 | (if (> (. self.appearances t) 1)
132 | (puts self "#<" id ">")
133 | (and (= (# non-seq-keys) 0) (= (# t) 0))
134 | (puts self "{}")
135 | (= (# non-seq-keys) 0)
136 | (put-sequential-table self t length)
137 | :else
138 | (put-kv-table self t))))))
139 |
140 | (set put-value (fn [self v]
141 | (let [tv (type v)]
142 | (if (= tv "string")
143 | (puts self (quote (escape v)))
144 | (or (= tv "number") (= tv "boolean") (= tv "nil"))
145 | (puts self (tostring v))
146 | (= tv "table")
147 | (put-table self v)
148 | :else
149 | (puts self "#<" tv " " (get-id self v) ">")))))
150 |
151 |
152 |
153 | (fn [root options]
154 | (let [options (or options {})
155 | inspector {:appearances (count-table-appearances root {})
156 | :depth (or options.depth 128)
157 | :level 0 :buffer {} :ids {} :max-ids {}
158 | :indent (or options.indent " ")}]
159 | (put-value inspector root)
160 | (table.concat inspector.buffer)))
161 |
--------------------------------------------------------------------------------
/fennel_preamble.lua:
--------------------------------------------------------------------------------
1 | package.path = './extern/?.lua;./extern/?/init.lua;' .. package.path
2 | local fennel = require('fennel')
3 | fennel.path = './extern/?.fnl;./extern/?/init.fnl;' .. fennel.path
4 | table.insert(package.searchers, fennel.searcher)
5 |
--------------------------------------------------------------------------------
/install-dependencies.sh:
--------------------------------------------------------------------------------
1 | # Install Lua, Luarocks, and otouto dependencies. Works in Ubuntu, maybe Debian.
2 | # Installs Lua 5.3 if Ubuntu 16.04. Otherwise, 5.2.
3 |
4 | #!/bin/sh
5 |
6 | rocklist="dkjson lpeg lrexlib-pcre luasec luasocket multipart-post serpent"
7 | if [ $(lsb_release -r | cut -f 2) == "16.04" ]; then
8 | luaver="5.3"
9 | else
10 | luaver="5.2"
11 | rocklist="$rocklist luautf8"
12 | fi
13 |
14 | echo "This script is intended for Ubuntu. It may work in Debian."
15 | echo "This script will request root privileges to install the following packages:"
16 | echo "lua$luaver liblua$luaver-dev fortune-mod fortunes git libc6 libpcre3-dev libssl-dev make unzip"
17 | echo "It will also request root privileges to install Luarocks to /usr/local/"
18 | echo "along with the following rocks:"
19 | echo $rocklist
20 | echo "Press enter to continue. Use Ctrl-C to exit."
21 | read
22 |
23 | sudo apt-get update
24 | sudo apt-get install -y lua$luaver liblua$luaver-dev fortune-mod fortunes git libc6 libpcre3-dev libssl-dev make unzip
25 | git clone http://github.com/keplerproject/luarocks
26 | cd luarocks
27 | ./configure --lua-version=$luaver --versioned-rocks-dir --lua-suffix=$luaver
28 | make build
29 | sudo make install
30 | for rock in $rocklist; do
31 | sudo luarocks-$luaver install $rock
32 | done
33 | sudo -k
34 | cd ..
35 |
36 | echo "Finished. Use ./launch to start otouto."
37 | echo "Be sure to set your bot token in config.lua."
38 |
--------------------------------------------------------------------------------
/launch.sh:
--------------------------------------------------------------------------------
1 | # Run otouto in Lua 5.3, if available.
2 | # (Specifying lua5.3 because "lua" is not linked to it in Ubuntu 16.04.)
3 | # Otherwise, use any generic installed Lua.
4 | # If none, give an error and a friendly suggestion.
5 | # If Lua was found, restart otouto five seconds after halting each time.
6 |
7 | #!/bin/sh
8 |
9 | # Ubuntu 16.04 seems to not link "lua" to lua5.3.
10 | if type lua5.3 >/dev/null 2>/dev/null; then
11 | while true; do
12 | lua5.3 main.lua
13 | echo "otouto has stopped. ^C to exit."
14 | sleep 5s
15 | done
16 | elif type lua >/dev/null 2>/dev/null; then
17 | while true; do
18 | lua main.lua
19 | echo "otouto has stopped. ^C to exit."
20 | sleep 5s
21 | done
22 | else
23 | echo "Lua not found."
24 | echo "If you're on Ubuntu, try running ./install-dependencies.sh."
25 | fi
26 |
--------------------------------------------------------------------------------
/main.lua:
--------------------------------------------------------------------------------
1 | if package.path:find('%./%?%.lua') and not package.path:find('%./%?/init%.lua') then
2 | package.path = package.path .. ';./?/init.lua'
3 | end
4 | dofile('fennel_preamble.lua')
5 | -- Fennel loaded
6 |
7 | local bot = require('otouto.bot')
8 |
9 | local instance = setmetatable({
10 | config = require('config')
11 | }, {__index = bot})
12 |
13 | return instance:run()
14 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/add_admin.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | add_admin.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 | local autils = require('otouto.autils')
9 |
10 | local P = {}
11 |
12 | function P:init(bot)
13 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
14 | :t('admin', true):t('add_?admin', true).table
15 | self.privilege = 5
16 | self.command = 'admin'
17 | self.doc = 'Promotes a user or users to administrator(s).'
18 | self.targeting = true
19 | end
20 |
21 | function P:action(bot, msg, _group, _user)
22 | local targets, output = autils.targets(bot, msg, {unknown_ids_err = true})
23 | for target, _ in pairs(targets) do
24 | local user = utilities.user(bot, target)
25 | if user:rank(bot, msg.chat.id) > 3 then
26 | table.insert(output, user:name() .. ' is already an administrator.')
27 | else
28 | user.data.administrator = true
29 | table.insert(output, user:name() .. ' is now an administrator.')
30 | end
31 | end
32 | utilities.send_reply(msg, table.concat(output, '\n'), 'html')
33 | end
34 |
35 | P.list = {
36 | name = 'admins',
37 | title = 'Global Administrators',
38 | type = 'userdata',
39 | key = 'administrator'
40 | }
41 |
42 | return P
43 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/add_group.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | add_group.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local anise = require('anise')
8 |
9 | local bindings = require('extern.bindings')
10 | local utilities = require('otouto.utilities')
11 |
12 | local P = {}
13 |
14 | function P:init(bot)
15 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
16 | :t('add_?group').table
17 | self.command = 'addgroup'
18 | self.doc = 'Adds the current supergroup to the administrative system.'
19 |
20 | self.privilege = 4
21 | end
22 |
23 | function P:action(bot, msg, group)
24 | local output
25 |
26 | if msg.chat.type ~= 'supergroup' then
27 | output = 'Administrated groups must be supergroups.'
28 |
29 | else
30 | local perms, group_owner
31 | local _, res = bindings.getChatAdministrators{ chat_id = msg.chat.id }
32 | for _, administrator in pairs(res.result) do
33 | if administrator.user.id == bot.info.id then
34 | perms = administrator
35 | elseif administrator.status == 'creator' then
36 | group_owner = administrator.user.id
37 | end
38 | end
39 |
40 | if not perms or not (
41 | perms.can_change_info and perms.can_delete_messages and
42 | perms.can_restrict_members and perms.can_promote_members and
43 | perms.can_invite_users
44 | ) then
45 | output =
46 | 'I must have permission to change group info, delete messages,'
47 | .. ' and add, ban, and promote members.'
48 | elseif group.data.admin then
49 | output = 'I am already administrating this group.'
50 | else
51 | -- This shouldn't fail; we have already checked permissions above.
52 | local _, lres = bindings.exportChatInviteLink{chat_id = msg.chat.id}
53 | group.data.admin = {
54 | link = lres.result,
55 | governor = group_owner or msg.from.id,
56 | owner = group_owner,
57 | rules = {},
58 | filter = {},
59 | antihammer = {},
60 | strikes = {},
61 | moderators = {},
62 | bans = {},
63 | flags = anise.clone(bot.config.administration.flags)
64 | }
65 |
66 | bindings.setChatDescription{
67 | chat_id = msg.chat.id,
68 | description = 'Welcome! Please review the rules and other group info with '
69 | .. bot.config.cmd_pat .. 'description@' ..
70 | bot.info.username .. '.'
71 | }
72 |
73 | output = 'I am now administrating this group.'
74 | end
75 | end
76 |
77 | utilities.send_reply(msg, output)
78 | end
79 |
80 | P.list = {
81 | name = 'administrated',
82 | title = 'Administrated Groups',
83 | type = 'groupdata',
84 | key = 'admin',
85 | sudo = true
86 | }
87 |
88 | return P
89 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/add_mod.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | add_mod.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 | local autils = require('otouto.autils')
9 |
10 | local P = {}
11 |
12 | function P:init(bot)
13 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
14 | :t('mod', true):t('add_?mod', true):t('op', true).table
15 | self.command = 'mod'
16 | self.doc = 'Promotes a user to a moderator.'
17 | self.privilege = 3
18 | self.administration = true
19 | self.targeting = true
20 | end
21 |
22 | function P:action(bot, msg, group)
23 | local targets, output = autils.targets(bot, msg, {unknown_ids_err = true})
24 | for target, _ in pairs(targets) do
25 | local name = utilities.lookup_name(bot, target)
26 | local rank = autils.rank(bot, target, msg.chat.id)
27 |
28 | if rank > 2 then
29 | autils.promote_admin(msg.chat.id, target, true)
30 | table.insert(output, name ..' is greater than a moderator.')
31 | else
32 | autils.promote_admin(msg.chat.id, target)
33 | local admin = group.data.admin
34 | if admin.moderators[target] then
35 | table.insert(output, name .. ' is already a moderator.')
36 | else
37 | admin.moderators[target] = true
38 | admin.bans[target] = nil
39 | table.insert(output, name .. ' is now a moderator.')
40 | end
41 | end
42 | end
43 | utilities.send_reply(msg, table.concat(output, '\n'), 'html')
44 | end
45 |
46 | P.list = {
47 | name = 'mods',
48 | title = 'Moderators',
49 | type = 'admin',
50 | key = 'moderators'
51 | }
52 |
53 | return P
54 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/antibot.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | antibot.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local bindings = require('extern.bindings')
8 | local autils = require('otouto.autils')
9 |
10 | local P = {}
11 |
12 | function P:init(bot)
13 | local flags_plugin = bot.named_plugins['admin.flags']
14 | assert(flags_plugin, self.name .. ' requires flags')
15 | self.flag = 'antibot'
16 | self.flag_desc = 'Only moderators may add bots.'
17 | flags_plugin.flags[self.flag] = self.flag_desc
18 | self.triggers = { '^$' }
19 | self.administration = true
20 | end
21 |
22 | function P:action(bot, msg, group, user)
23 | if
24 | group.data.admin.flags[self.flag]
25 | and msg.new_chat_member
26 | and msg.new_chat_member.is_bot
27 | and user:rank(bot, msg.chat.id) < 2
28 | then
29 | if bindings.kickChatMember{
30 | chat_id = msg.chat.id,
31 | user_id = msg.new_chat_member.id
32 | } then
33 | autils.log(bot, {
34 | chat_id = msg.chat.id,
35 | target = msg.new_chat_member.id,
36 | action = 'Bot removed',
37 | source = self.flag,
38 | reason = self.flag_desc
39 | })
40 | end
41 | else
42 | return 'continue'
43 | end
44 | end
45 |
46 | return P
47 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/antihammer_whitelist.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | antihammer_whitelist.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 | local autils = require('otouto.autils')
9 |
10 | local P = {}
11 |
12 | function P:init(bot)
13 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
14 | :t('antihammer', true).table
15 | self.command = 'antihammer'
16 | self.doc = "Returns a list of users who are protected from global bans in \z
17 | this group, or toggle the status of a target or targets.."
18 | self.privilege = 3
19 | self.administration = true
20 | self.targeting = true
21 | end
22 |
23 | function P:action(bot, msg, group)
24 | local targets, output = autils.targets(bot, msg)
25 | local admin = group.data.admin
26 |
27 | if #targets > 0 or #output > 0 then
28 | for target, _ in pairs(targets) do
29 | local name = utilities.lookup_name(bot, target)
30 | if admin.antihammer[target] then
31 | admin.antihammer[target] = nil
32 | table.insert(output, name ..
33 | ' has been removed from the antihammer whitelist.')
34 | else
35 | admin.antihammer[target] = true
36 | table.insert(output, name ..
37 | ' has been added to the antihammer whitelist.')
38 | end
39 | end
40 |
41 | elseif next(admin.antihammer) then
42 | table.insert(output, 'Antihammered users:')
43 | table.insert(output, '• ' ..
44 | table.concat(utilities.list_names(bot, admin.antihammer), '\n• '))
45 |
46 | else
47 | table.insert(output, 'There are no antihammer-whitelisted users.')
48 | end
49 |
50 | utilities.send_reply(msg, table.concat(output, '\n'), 'html')
51 | end
52 |
53 | return P
54 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/antilink.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | antilink.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local anise = require('anise')
8 |
9 | local bindings = require('extern.bindings')
10 | local autils = require('otouto.autils')
11 |
12 | local P = {}
13 |
14 | function P:init(bot)
15 | local flags_plugin = bot.named_plugins['admin.flags']
16 | assert(flags_plugin, self.name .. ' requires flags')
17 | self.flag = 'antilink'
18 | self.flag_desc = 'Posting links to other groups is not allowed.'
19 | flags_plugin.flags[self.flag] = self.flag_desc
20 |
21 | self.help_word = 'antilink'
22 | self.doc = "\z
23 | antilink checks links and usernames posted by non-moderators. If a message \z
24 | references a group or channel outside the realm, an automoderation strike is \z
25 | issued (see /help automoderation) and the user's global antilink counter is \z
26 | incremented. When his counter reaches 3, he is globally banned and \z
27 | immediately removed from all groups where antilink was triggered. \
28 | antilink can be enabled with /flag antilink (see /help flags)."
29 |
30 | -- Build the antilink patterns. Additional future domains can be added to
31 | -- this list to keep it up to date.
32 | self.patterns = {}
33 | for _, domain in pairs{
34 | 'telegram.me',
35 | 'telegram.dog',
36 | 'tlgrm.me',
37 | 't.me',
38 | 'telega.one'
39 | } do
40 | local s = ''
41 | -- We build the pattern character by character from the domains.
42 | -- May become an issue when emoji TLDs become mainstream. ;)
43 | for char in domain:gmatch('.') do
44 | if char:match('%l') then
45 | s = s .. '[' .. char:upper() .. char .. ']'
46 | -- all characters which must be escaped
47 | elseif char:match('[%%%.%^%$%+%-%*%?]') then
48 | s = s .. '%' .. char
49 | else
50 | s = s .. char
51 | end
52 | end
53 | table.insert(self.patterns, s)
54 | end
55 | self.triggers = anise.clone(self.patterns)
56 | table.insert(self.triggers, '@[%w_]+')
57 |
58 | -- Infractions are stored, and users are globally banned after three within
59 | -- one day of each other.
60 | if not bot.database.userdata.antilink then
61 | bot.database.userdata.antilink = {}
62 | end
63 |
64 | self.administration = true
65 | end
66 |
67 | function P:action(bot, msg, group, user)
68 | local admin = group.data.admin
69 | if not admin.flags[self.flag] then return 'continue' end
70 | if user:rank(bot, msg.chat.id) > 1 then return 'continue' end
71 | if msg.forward_from and (
72 | (msg.forward_from.id == bot.info.id) or
73 | (msg.forward_from.id == bot.config.log_chat) or
74 | (msg.forward_from.id == bot.config.administration.log_chat)
75 | ) then
76 | return 'continue'
77 | end
78 | if self:check(bot, msg) then
79 | local store = user.data.antilink
80 | if not store then
81 | store = {
82 | count = 0,
83 | groups = {},
84 | }
85 | user.data.antilink = store
86 | end
87 | store.count = store.count + 1
88 | store.groups[tostring(msg.chat.id)] = true
89 |
90 | bot:do_later(self.name, os.time() + 86400, msg.from.id)
91 |
92 | if store.count == 3 then
93 | user.data.hammered = true
94 | bindings.deleteMessage{ chat_id = msg.chat.id,
95 | message_id = msg.message_id }
96 | autils.log(bot, {
97 | chat_id = not admin.flags.private and msg.chat.id,
98 | target = msg.from.id,
99 | action = 'Globally banned',
100 | source = self.flag,
101 | reason = self.flag_desc
102 | })
103 | for chat_id_str, _ in pairs(store.groups) do
104 | bindings.kickChatMember{
105 | chat_id = chat_id_str,
106 | user_id = msg.from.id
107 | }
108 | end
109 | user.data.antilink = nil
110 | else
111 | autils.strike(bot, msg, self.flag)
112 | end
113 | else
114 | return 'continue'
115 | end
116 | end
117 |
118 | function P:later(bot, user_id)
119 | local store = bot.database.userdata.antilink[tostring(user_id)]
120 | if store then
121 | if store.count > 1 then
122 | store.count = store.count - 1
123 | else
124 | bot.database.userdata.antilink[tostring(user_id)] = nil
125 | end
126 | end
127 | end
128 |
129 | P.edit_action = P.action
130 |
131 | -- Links can come from the message text or from entities, and can be joinchat
132 | -- links (t.me/joinchat/abcdefgh), username links (t.me/abcdefgh), or usernames
133 | -- (@abcdefgh).
134 | function P:check(bot, msg)
135 | for _, pattern in pairs(self.patterns) do
136 |
137 | -- Iterate through links in the message, and determine if they refer to
138 | -- external groups.
139 | for link in msg.text:gmatch(pattern..'%g*') do
140 | if self:parse_and_detect(bot, link, pattern) then
141 | return true
142 | end
143 | end
144 |
145 | -- Iterate through the messages's entities, if any, and determine if
146 | -- they're links to external groups.
147 | if msg.entities then
148 | for _, entity in ipairs(msg.entities) do
149 | if entity.url and self:parse_and_detect(bot, entity.url, pattern) then
150 | return true
151 | end
152 | end
153 | end
154 | end
155 |
156 | -- Iterate through all usernames in the message text, and determine if they
157 | -- are external group links.
158 | for username in msg.text:gmatch('@([%w_]+)') do
159 | if
160 | not (msg.forward_from_chat and username == msg.forward_from_chat.username)
161 | and self:is_username_external(bot, username)
162 | then
163 | return true
164 | end
165 | end
166 | end
167 |
168 | -- This function takes a link or username (parsed from a message or found in an
169 | -- entity) and returns true if that link or username refers to a supergroup
170 | -- outside of the realm.
171 | function P:parse_and_detect(bot, link, pattern)
172 | local code = link:match(pattern .. -- /joinchat/ABC-def_123
173 | '/[Jj][Oo][Ii][Nn][Cc][Hh][Aa][Tt]/([%w_%-]+)')
174 | local username = link:match(pattern .. '/([%w_]+)')
175 | if (code and self:is_code_external(bot, code)) or
176 | (username and self:is_username_external(bot, username))
177 | then
178 | return true
179 | end
180 | end
181 |
182 | -- This function determines whether or not a given joinchat "code" refers to
183 | -- a group outside the realm (true/false)
184 | function P:is_code_external(bot, code)
185 | -- Prepare the code to be used as a pattern by escaping any hyphens.
186 | -- Also, add an anchor.
187 | local pattern = '/' .. code:gsub('%-', '%%-') .. '$'
188 | -- Iterate through groups and return false if the joinchat code belongs to
189 | -- any one of them.
190 | for _, group in pairs(bot.database.groupdata.admin) do
191 | if group.link:match(pattern) then
192 | return false
193 | end
194 | end
195 | return true
196 | end
197 |
198 | -- This function determines whether or not a username refers to a supergroup
199 | -- outside the realm (true/false).
200 | function P:is_username_external(bot, username)
201 | local suc, res = bindings.getChat{chat_id = '@' .. username}
202 | -- If the username is an external supergroup or channel, return true.
203 | if suc and (res.result.type=='supergroup' or res.result.type=='channel') and
204 | not bot.database.groupdata.admin[tostring(res.result.id)] then
205 | return true
206 | end
207 | return false
208 | end
209 |
210 | return P
211 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/antisquig.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | antisquig.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 | local autils = require('otouto.autils')
9 |
10 | local P = {}
11 |
12 | function P:init(bot)
13 | local flags_plugin = bot.named_plugins['admin.flags']
14 | assert(flags_plugin, self.name .. ' requires flags')
15 | self.flag = 'antisquig'
16 | flags_plugin.flags[self.flag] =
17 | 'Arabic script is not allowed in messages.'
18 | self.triggers = {
19 | utilities.char.arabic,
20 | utilities.char.rtl_override,
21 | utilities.char.rtl_mark
22 | }
23 | self.administration = true
24 | end
25 |
26 | function P:action(bot, msg, group, user)
27 | if not group.data.admin.flags[self.flag] then return 'continue' end
28 | if user:rank(bot, msg.chat.id) > 1 then return 'continue' end
29 | if msg.forward_from and (
30 | msg.forward_from.id == bot.info.id or
31 | msg.forward_from.id == bot.config.log_chat or
32 | msg.forward_from.id == bot.config.administration.log_chat
33 | ) then
34 | return 'continue'
35 | end
36 |
37 | autils.strike(bot, msg, self.flag)
38 | end
39 |
40 | P.edit_action = P.action
41 |
42 | return P
43 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/antisquigpp.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | antisquigpp.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 | local autils = require('otouto.autils')
9 | local bindings = require('extern.bindings')
10 |
11 | local P = {}
12 |
13 | function P:init(bot)
14 | local flags_plugin = bot.named_plugins['admin.flags']
15 | assert(flags_plugin, self.name .. ' requires flags')
16 | self.flag_desc = 'Arabic script is not allowed in names.'
17 | self.flag = 'antisquigpp'
18 | flags_plugin.flags[self.flag] = self.flag_desc
19 | self.triggers = {''}
20 | self.administration = true
21 | end
22 |
23 | function P:action(bot, msg, group, user)
24 | if not group.data.admin.flags[self.flag] then return 'continue' end
25 | if user:rank(bot, msg.chat.id) > 1 then return 'continue' end
26 | local name = utilities.build_name(user.data.info.first_name, user.data.info.last_name)
27 | if name:match(utilities.char.arabic) or
28 | name:match(utilities.char.rtl_override) or
29 | name:match(utilities.char.rtl_mark)
30 | then
31 | bindings.deleteMessage{
32 | chat_id = msg.chat.id,
33 | message_id = msg.message_id
34 | }
35 |
36 | local success, result = bindings.kickChatMember{
37 | chat_id = msg.chat.id,
38 | user_id = msg.from.id
39 | }
40 |
41 | autils.log(bot, {
42 | source = self.flag,
43 | reason = self.flag_desc,
44 | target = msg.from.id,
45 | chat_id = msg.chat.id,
46 | action = success and 'Kicked' or result.description
47 | })
48 | else
49 | return 'continue'
50 | end
51 | end
52 |
53 | return P
54 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/antisticker.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | nostickers.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local bindings = require('extern.bindings')
8 | local utilities = require('otouto.utilities')
9 | local autils = require('otouto.autils')
10 |
11 | local P = {}
12 |
13 | function P:init(bot)
14 | local flags_plugin = bot.named_plugins['admin.flags']
15 | assert(flags_plugin, self.name .. ' requires flags')
16 | self.flag = 'antisticker'
17 | self.flag_desc = 'Stickers are filtered.'
18 | flags_plugin.flags[self.flag] = self.flag_desc
19 | self.triggers = {'^$'}
20 | self.administration = true
21 | end
22 |
23 | function P:action(bot, msg, group)
24 | local admin = group.data.admin
25 | if admin.flags[self.flag] and msg.sticker then
26 | bindings.deleteMessage{
27 | message_id = msg.message_id,
28 | chat_id = msg.chat.id
29 | }
30 |
31 | if msg.date >= (admin.last_antisticker_msg or -3600) + 3600 then -- 1h
32 | local success, result =
33 | utilities.send_message(msg.chat.id, 'Stickers are filtered.')
34 | if success then
35 | bot:do_later('core.delete_messages', os.time() + 5, {
36 | chat_id = msg.chat.id,
37 | message_id = result.result.message_id
38 | })
39 | admin.last_antisticker_msg = result.result.date
40 | end
41 | end
42 |
43 | autils.log(bot, {
44 | chat_id = msg.chat.id,
45 | target = msg.from.id,
46 | action = 'Sticker deleted',
47 | source = self.flag,
48 | reason = self.flag_desc
49 | })
50 | else
51 | return 'continue'
52 | end
53 | end
54 |
55 | return P
56 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/autopromoter.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | autopromoter.lua
3 | Promotes mods & admins when they join a group.
4 | Copyright 2018 topkecleon
5 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 | ]]--
7 |
8 | local autils = require('otouto.autils')
9 |
10 | local P = {}
11 |
12 | function P:init(_bot)
13 | self.triggers = {'^$'}
14 | self.administration = true
15 | end
16 |
17 | function P:action(bot, msg, _group, _user)
18 | if msg.new_chat_member then
19 | local rank = autils.rank(bot, msg.new_chat_member.id, msg.chat.id)
20 | if rank > 1 then
21 | autils.promote_admin(msg.chat.id, msg.new_chat_member.id, rank > 2)
22 | end
23 | end
24 | return 'continue'
25 | end
26 |
27 | return P
28 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/ban.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | ban.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local anise = require('anise')
8 | local bindings = require('extern.bindings')
9 | local utilities = require('otouto.utilities')
10 | local autils = require('otouto.autils')
11 |
12 | local P = {}
13 |
14 | function P:init(bot)
15 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
16 | :t('ban', true).table
17 | self.command = 'ban'
18 | self.doc = "Bans a user or users from the group. Targets can be unbanned \z
19 | with /unban. A reason can be given on a new line. Example:\
20 | /ban @examplus 5551234\
21 | Bad jokes."
22 |
23 | self.privilege = 2
24 | self.administration = true
25 | self.targeting = true
26 | end
27 |
28 | function P:action(bot, msg, group)
29 | local admin = group.data.admin
30 | local targets, output, reason = autils.targets(bot, msg)
31 | local banned_users = anise.set()
32 |
33 | for target, _ in pairs(targets) do
34 | local name = utilities.lookup_name(bot, target)
35 | if autils.rank(bot, target, msg.chat.id) >= 2 then
36 | table.insert(output, name .. ' is too privileged to be banned.')
37 | elseif admin.bans[target] then
38 | table.insert(output, name .. ' is already banned.')
39 | else
40 | bindings.kickChatMember{
41 | chat_id = msg.chat.id,
42 | user_id = target
43 | }
44 | admin.bans[target] = reason or true
45 | banned_users:add(target, reason or true)
46 | table.insert(output, name .. ' has been banned.')
47 | end
48 | end
49 |
50 | utilities.send_reply(msg, table.concat(output, '\n'), 'html')
51 | if #banned_users > 0 then
52 | autils.log(bot, {
53 | chat_id = msg.chat.id,
54 | targets = banned_users,
55 | action = 'Banned',
56 | source_user = msg.from,
57 | reason = reason
58 | })
59 | end
60 | end
61 |
62 | P.list = {
63 | name = 'banned',
64 | title = 'Banned Users',
65 | type = 'admin',
66 | key = 'bans'
67 | }
68 |
69 | return P
70 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/ban_remover.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | ban_remover.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local bindings = require('extern.bindings')
8 | local autils = require('otouto.autils')
9 |
10 | local P = {}
11 |
12 | function P:init(_bot)
13 | self.triggers = {''}
14 | self.administration = true
15 | end
16 |
17 | function P:action(bot, msg, _group, user)
18 | if user:rank(bot, msg.chat.id) == 0 then
19 | bindings.kickChatMember{
20 | chat_id = msg.chat.id,
21 | user_id = msg.from.id
22 | }
23 | bindings.deleteMessage{
24 | chat_id = msg.chat.id,
25 | message_id = msg.message_id
26 | }
27 | autils.log(bot, {
28 | chat_id = msg.chat.id,
29 | target = msg.from.id,
30 | action = 'Kicked and message deleted',
31 | source = self.name,
32 | reason = 'User is banned.'
33 | })
34 | if msg.new_chat_member then
35 | bindings.kickChatMember {
36 | chat_id = msg.chat.id,
37 | user_id = msg.new_chat_member.id
38 | }
39 | end
40 | elseif msg.new_chat_member then
41 | if autils.rank(bot, msg.new_chat_member.id, msg.chat.id) == 0 then
42 | bindings.kickChatMember{
43 | chat_id = msg.chat.id,
44 | user_id = msg.new_chat_member.id
45 | }
46 | bindings.deleteMessage{
47 | chat_id = msg.chat.id,
48 | message_id = msg.message_id
49 | }
50 | autils.log(bot, {
51 | chat_id = msg.chat.id,
52 | target = msg.new_chat_member.id,
53 | action = 'Kicked',
54 | source = self.name,
55 | reason = 'User is banned.'
56 | })
57 | else
58 | return 'continue'
59 | end
60 | else
61 | return 'continue'
62 | end
63 | end
64 |
65 | return P
66 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/deadmin.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | deadmin.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 | local autils = require('otouto.autils')
9 |
10 | local P = {}
11 |
12 | function P:init(bot)
13 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
14 | :t('deadmin', true).table
15 | self.privilege = 5
16 | self.command = 'deadmin'
17 | self.doc = 'Demotes an administrator or administrators.'
18 | self.targeting = true
19 | end
20 |
21 | function P:action(bot, msg, _group, _user)
22 | local targets, output = autils.targets(bot, msg)
23 | for target, _ in pairs(targets) do
24 | local user = utilities.user(bot, target)
25 | if user.data.administrator then
26 | user.data.administrator = nil
27 | for chat_id, _ in pairs(bot.database.groupdata.admin) do
28 | if user:rank(bot, chat_id) < 2 then
29 | autils.demote_admin(chat_id, target)
30 | end
31 | end
32 | table.insert(output, user:name() .. ' is no longer an administrator.')
33 | else
34 | table.insert(output, user:name() .. ' is not an administrator.')
35 | end
36 | end
37 | utilities.send_reply(msg, table.concat(output, '\n'), 'html')
38 | end
39 |
40 | return P
41 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/delete_join_messages.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | delete_join_messages.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local bindings = require('extern.bindings')
8 |
9 | local P = {}
10 |
11 | function P:init(bot)
12 | local flags_plugin = bot.named_plugins['admin.flags']
13 | assert(flags_plugin, self.name .. ' requires flags')
14 | self.flag = 'delete_join_messages'
15 | flags_plugin.flags[self.flag] =
16 | 'Deletes new_chat_member messages. These deletions are not logged.'
17 | self.triggers = { '^$' }
18 | self.administration = true
19 | end
20 |
21 | function P:action(_bot, msg, group, _user)
22 | if not group.data.admin.flags[self.flag] then return 'continue' end
23 | if msg.new_chat_members and msg.from.id == msg.new_chat_members[1].id then
24 | bindings.deleteMessage{
25 | chat_id = msg.chat.id,
26 | message_id = msg.message_id
27 | }
28 | else
29 | return 'continue'
30 | end
31 | end
32 |
33 | return P
34 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/delete_left_messages.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | delete_left_messages.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local bindings = require('extern.bindings')
8 |
9 | local P = {}
10 |
11 | function P:init(bot)
12 | local flags_plugin = bot.named_plugins['admin.flags']
13 | assert(flags_plugin, self.name .. ' requires flags')
14 | self.flag = 'delete_left_messages'
15 | flags_plugin.flags[self.flag] =
16 | 'Deletes left_chat_member messages. These deletions are not logged.'
17 | self.triggers = { '^$' }
18 | self.administration = true
19 | end
20 |
21 | function P:action(_bot, msg, group, _user)
22 | if not group.data.admin.flags[self.flag] then return 'continue' end
23 | if msg.left_chat_member and msg.from.id == msg.left_chat_member.id then
24 | bindings.deleteMessage{
25 | chat_id = msg.chat.id,
26 | message_id = msg.message_id
27 | }
28 | else
29 | return 'continue'
30 | end
31 | end
32 |
33 | return P
34 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/demod.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | demod.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 | local autils = require('otouto.autils')
9 |
10 | local P = {}
11 |
12 | function P:init(bot)
13 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
14 | :t('demod', true).table
15 | self.command = 'demod'
16 | self.doc = 'Demotes a moderator.'
17 | self.privilege = 3
18 | self.administration = true
19 | self.targeting = true
20 | end
21 |
22 | function P:action(bot, msg, group)
23 | local targets, output = autils.targets(bot, msg)
24 | for target, _ in pairs(targets) do
25 | local name = utilities.lookup_name(bot, target)
26 | local admin = group.data.admin
27 | if autils.rank(bot, target, msg.chat.id) < 3 then
28 | autils.demote_admin(msg.chat.id, target)
29 | end
30 | if admin.moderators[target] then
31 | admin.moderators[target] = nil
32 | table.insert(output, name .. ' is no longer a moderator.')
33 | else
34 | table.insert(output, name .. ' is not a moderator.')
35 | end
36 | end
37 | utilities.send_reply(msg, table.concat(output, '\n'), 'html')
38 | end
39 |
40 | return P
41 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/files_only.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | filesonly.lua
3 | A flag to delete photos, videos, gifs, etc, and reupload them as files.
4 | Copyright 2018 topkecleon
5 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 | ]]--
7 |
8 | local bindings = require('extern.bindings')
9 | local utilities = require('otouto.utilities')
10 |
11 | local P = {}
12 |
13 | function P:init(bot)
14 | local flags_plugin = bot.named_plugins['admin.flags']
15 | assert(flags_plugin, self.name .. ' requires flags')
16 | self.flag = 'files_only'
17 | self.flag_desc = 'Deletes photos and videos and reuploads them as files.'
18 | flags_plugin.flags[self.flag] = self.flag_desc
19 | self.triggers = {''}
20 | self.administration = true
21 | end
22 |
23 | function P:action(bot, msg, group)
24 | if not group.data.admin.flags[self.flag] then
25 | return 'continue'
26 | end
27 |
28 | local file_id
29 | if msg.photo then
30 | file_id = msg.photo[#msg.photo].file_id
31 | elseif msg.video_note then
32 | file_id = msg.video_note.file_id
33 | end
34 |
35 | if file_id then
36 | local success, result = bindings.getFile{file_id = file_id}
37 | if success then
38 | local filename = utilities.download_file(
39 | 'https://api.telegram.org/file/bot' .. bot.config.bot_api_key
40 | .. '/' .. result.result.file_path,
41 | '/tmp/' .. os.time() .. result.result.file_path:match('%..-$')
42 | )
43 | local caption = 'Media from ' .. utilities.print_name(msg.from)
44 | if msg.caption then
45 | caption = caption .. ':\n' .. msg.caption
46 | end
47 | if bindings.sendDocument(
48 | {chat_id = msg.chat.id, caption = caption},
49 | {document = filename}
50 | ) then
51 | bindings.deleteMessage{
52 | chat_id = msg.chat.id,
53 | message_id = msg.message_id
54 | }
55 | end
56 | os.execute('rm ' .. filename)
57 | return
58 | end
59 | end
60 |
61 | return 'continue'
62 | end
63 |
64 | return P
65 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/filter.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | filter.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 |
9 | local P = {}
10 |
11 | function P:init(bot)
12 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
13 | :t('filter', true).table
14 | self.command = 'filter [term]'
15 | self.doc = "Adds or removes a filter, or lists all filters. Messages containing filtered terms are deleted. \z
16 | Filters use Lua patterns."
17 | self.privilege = 3
18 | self.administration = true
19 | end
20 |
21 | function P:action(_bot, msg, group, _user)
22 | local admin = group.data.admin
23 | local input = utilities.input(msg.text_lower)
24 | local output
25 | if input then
26 | local idx
27 | for i = 1, #admin.filter do
28 | if admin.filter[i] == input then
29 | idx = i
30 | break
31 | end
32 | end
33 | if idx then
34 | table.remove(admin.filter, idx)
35 | output = 'That term has been removed from the filter.'
36 | else
37 | table.insert(admin.filter, input)
38 | output = 'That term has been added to the filter.'
39 | end
40 | elseif #admin.filter == 0 then
41 | output = 'There are currently no filtered terms.'
42 | else
43 | output = 'Filtered terms:\n• ' ..
44 | utilities.html_escape(table.concat(admin.filter, '\n• '))
45 | end
46 | utilities.send_reply(msg, output, 'html')
47 | end
48 |
49 | return P
50 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/filterer.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | filterer.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local bindings = require('extern.bindings')
8 | local autils = require('otouto.autils')
9 | local utilities = require('otouto.utilities')
10 | local rot13 = require('otouto.rot13')
11 |
12 | local P = {}
13 |
14 | function P:init(_bot)
15 | self.triggers = {''}
16 | self.administration = true
17 | end
18 |
19 | function P:action(bot, msg, group, user)
20 | if user:rank(bot, msg.chat.id) > 1 then return 'continue' end
21 | if msg.forward_from and (
22 | (msg.forward_from.id == bot.info.id) or
23 | (msg.forward_from.id == bot.config.log_chat) or
24 | (msg.forward_from.id == bot.config.administration.log_chat)
25 | ) then
26 | return 'continue'
27 | end
28 |
29 | local admin = group.data.admin
30 | for i = 1, #admin.filter do
31 | if msg.text_lower:match(admin.filter[i]) then
32 | bindings.deleteMessage{
33 | message_id = msg.message_id,
34 | chat_id = msg.chat.id
35 | }
36 |
37 | if msg.date >= (admin.last_filter_msg or -3600) + 3600 then -- 1h
38 | local success, result = utilities.send_message(msg.chat.id,
39 | 'Deleted a filtered term.')
40 | if success then
41 | bot:do_later('core.delete_messages', os.time() + 5, {
42 | chat_id = msg.chat.id,
43 | message_id = result.result.message_id
44 | })
45 | admin.last_filter_msg = result.result.date
46 | end
47 | end
48 |
49 | autils.log(bot, {
50 | chat_id = msg.chat.id,
51 | target = msg.from.id,
52 | action = 'Message deleted',
53 | source = self.name,
54 | reason = 'ROT13: ' ..
55 | utilities.html_escape(rot13.cipher(admin.filter[i]))
56 | })
57 | return
58 | end
59 | end
60 |
61 | return 'continue'
62 | end
63 |
64 | P.edit_action = P.action
65 |
66 | return P
67 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/fix_perms.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | fix_perms.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 | local autils = require('otouto.autils')
9 |
10 | local P = {}
11 |
12 | function P:init(bot)
13 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
14 | :t('fix_?perms', true).table
15 | self.command = 'fixperms'
16 | self.doc = 'Fixes local permissions for the user or specified target.'
17 | self.privilege = 2
18 | self.targeting = true
19 | self.administration = true
20 | end
21 |
22 | function P:action(bot, msg)
23 | local targets, output = autils.targets(bot, msg, {self_targeting = true})
24 | for target, _ in pairs(targets) do
25 | local rank = autils.rank(bot, target, msg.chat.id)
26 | local name = utilities.lookup_name(bot, target)
27 | local suc, res
28 | if rank >= 3 then
29 | suc, res = autils.promote_admin(msg.chat.id, target, true)
30 | elseif rank == 2 then
31 | suc, res = autils.promote_admin(msg.chat.id, target)
32 | else
33 | suc, res = autils.demote_admin(msg.chat.id, target)
34 | end
35 | if suc then
36 | table.insert(output,
37 | 'Permissions have been corrected for ' .. name .. '.')
38 | else
39 | table.insert(output, 'Error correcting permissions for ' ..
40 | name .. ': ' .. res.description)
41 | end
42 | end
43 | utilities.send_reply(msg, table.concat(output, '\n'), 'html')
44 | end
45 |
46 | return P
47 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/flags.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | flags.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local anise = require('anise')
8 | local utilities = require('otouto.utilities')
9 |
10 | local P = {}
11 |
12 | P.flags = {
13 | private = 'Removes the link from the public group list and suppresses logs.'
14 | }
15 |
16 | function P:init(bot)
17 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
18 | :t('flags?', true).table
19 | self.command = 'flags '
20 | self.help_word = 'flags?'
21 | self.privilege = 3
22 | self.administration = true
23 | self.doc = "Returns a list of flags, or toggles the specified flag. \
24 | Flags are administrative policies at the disposal of the governor. Most \z
25 | provide optional automoderation (see /help antilink). The private flag \z
26 | removes a group's link from the public list and makes it only available to \z
27 | moderators and greater."
28 | if anise.dict_len(bot.config.administration.flags) > 0 then
29 | self.doc = self.doc .. "\nThe following flags are enabled by default:"
30 | for flag, _ in pairs(bot.config.administration.flags) do
31 | self.doc = self.doc .. '\n• ' .. flag
32 | end
33 | end
34 | end
35 |
36 | function P:action(_bot, msg, group)
37 | local admin = group.data.admin
38 | local input = utilities.input_from_msg(msg)
39 | local output
40 |
41 | if not input then
42 | output = self:list_flags(admin.flags)
43 | .. '\n\nSpecify a flag or flags to toggle.'
44 | else
45 | input = input:lower()
46 | local out = {}
47 | for flagname in input:gmatch('%g+') do
48 | local escaped = utilities.html_escape(flagname)
49 | if self.flags[flagname] then
50 | if admin.flags[flagname] then
51 | admin.flags[flagname] = nil
52 | table.insert(out, 'Flag disabled: ' .. escaped .. '.')
53 | else
54 | admin.flags[flagname] = true
55 | table.insert(out, 'Flag enabled: ' .. escaped .. '.')
56 | end
57 | else
58 | table.insert(out, 'Not a valid flag name: ' .. escaped .. '.')
59 | end
60 | end
61 | output = table.concat(out, '\n')
62 | end
63 |
64 | utilities.send_reply(msg, output, 'html')
65 | end
66 |
67 | -- List flags under Enabled and Disabled.
68 | function P:list_flags(local_flags)
69 | local disabled_flags = {}
70 | for flag in pairs(self.flags) do
71 | if not local_flags[flag] then
72 | disabled_flags[flag] = true
73 | end
74 | end
75 | return string.format(
76 | 'Enabled flags:\n• %s\nDisabled flags:\n• %s',
77 | table.concat(self:flag_list(local_flags), '\n• '),
78 | table.concat(self:flag_list(disabled_flags), '\n• ')
79 | )
80 | end
81 |
82 | -- List flags.
83 | function P:flag_list(local_flags)
84 | local t = {}
85 | for flag in pairs(local_flags) do
86 | table.insert(t, flag .. ': ' .. self.flags[flag])
87 | end
88 | return t
89 | end
90 |
91 | -- Decrement a user's strikes in a group.
92 | function P:later(bot, params)
93 | -- Check if group is still administrated, otherwise this will break.
94 | if bot.database.groupdata.admin[tostring(params.chat_id)] then
95 | local store = bot.database.groupdata.admin[tostring(params.chat_id)].strikes
96 | local uis = tostring(params.user_id)
97 | if store[uis] then
98 | if store[uis] > 1 then
99 | store[uis] = store[uis] - 1
100 | else
101 | store[uis] = nil
102 | end
103 | end
104 | end
105 | end
106 |
107 | return P
108 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/get_description.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | get_description.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 |
9 | local P = {}
10 |
11 | function P:init(bot)
12 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
13 | :t('description', true):t('desc', true).table
14 | self.command = 'description [chat]'
15 | end
16 |
17 | function P:action(bot, msg, group)
18 | local input = utilities.input(msg.text_lower)
19 | local chat_id
20 | if input then
21 | for id_str, chat in pairs(bot.database.groupdata.admin) do
22 | if not chat.flags.private and bot.database.groupdata.info[id_str]
23 | .title:lower():find(input, 1, true) then
24 | chat_id = id_str
25 | break
26 | end
27 | end
28 | if not chat_id then
29 | utilities.send_reply(msg, 'Group not found.')
30 | return
31 | end
32 | elseif group and group.data.admin then
33 | chat_id = msg.chat.id
34 | else
35 | utilities.send_reply(msg, 'Specify a group.')
36 | return
37 | end
38 |
39 | local description = self.desc(bot, chat_id)
40 |
41 | if msg.chat.id == msg.from.id then
42 | utilities.send_reply(msg, description, 'html')
43 | else
44 | if utilities.send_message(msg.from.id, description, true, nil, 'html') then
45 | utilities.send_reply(msg, 'I have sent you the requested information in a private message.')
46 | else
47 | utilities.send_reply(msg, description, 'html')
48 | end
49 | end
50 | end
51 |
52 | function P.desc(bot, chat_id)
53 | local admin = bot.database.groupdata.admin[tostring(chat_id)]
54 | local output = {}
55 |
56 | -- Group title
57 | table.insert(output, utilities.lookup_name(bot, chat_id))
58 |
59 | -- Description
60 | table.insert(output, admin.description)
61 |
62 | -- Rules
63 | if #admin.rules > 0 then
64 | table.insert(output, 'Rules:\n' .. table.concat(
65 | bot.named_plugins['admin.list_rules'].rule_list(admin.rules), '\n'))
66 | end
67 |
68 | -- Flags
69 | if next(admin.flags) ~= nil then
70 | table.insert(output, 'Flags:\n• ' .. table.concat(
71 | bot.named_plugins['admin.flags']:flag_list(admin.flags), '\n• '))
72 | end
73 |
74 | -- Governor
75 | table.insert(output, 'Governor: ' ..
76 | utilities.lookup_name(bot, admin.governor))
77 |
78 | -- Moderators
79 | if next(admin.moderators) ~= nil then
80 | table.insert(output, 'Moderators:\n• ' .. table.concat(
81 | utilities.list_names(bot, admin.moderators), '\n• '))
82 | end
83 |
84 | return table.concat(output, '\n\n')
85 | end
86 |
87 | return P
88 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/get_link.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | get_link.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 |
9 | local P = {}
10 |
11 | function P:init(bot)
12 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
13 | :t('link'):t('get_?link').table
14 | self.command = 'link'
15 | self.doc = "Returns the group link. If the group is private, \z
16 | only moderators may use this command and responses will be sent in private."
17 | self.administration = true
18 | end
19 |
20 | function P:action(bot, msg, group, user)
21 | local admin = group.data.admin
22 | local output
23 | local link = string.format(
24 | '%s',
25 | admin.link,
26 | utilities.html_escape(msg.chat.title)
27 | )
28 |
29 | -- Links to private groups are mods+ and are only PM'd.
30 | if admin.flags.private then
31 | if user:rank(bot, msg.chat.id) > 1 then
32 | if utilities.send_message(msg.from.id, link, true, nil, 'html') then
33 | output = 'I have sent you the requested information in a private message.'
34 | else
35 | output = "This group is private. The link must be received privately. \z
36 | Please message me privately and re-run the command."
37 | end
38 | else
39 | output = 'This group is private. Only moderators may retrieve its link.'
40 | end
41 | else
42 | output = link
43 | end
44 |
45 | utilities.send_message(msg.chat.id, output, true, msg.message_id, 'html')
46 | end
47 |
48 | return P
49 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/hammer.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | hammer.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local anise = require('anise')
8 | local bindings = require('extern.bindings')
9 | local utilities = require('otouto.utilities')
10 | local autils = require('otouto.autils')
11 |
12 | local P = {}
13 |
14 | function P:init(bot)
15 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
16 | :t('hammer', true).table
17 | self.command = 'hammer'
18 | self.doc = "Globally bans a user or users. Targets can be unbanned with \z
19 | /unhammer. A reason can be given on a new line. Example:\
20 | /hammer @examplus 5556789\
21 | Really bad jokes."
22 | self.privilege = 4
23 | self.targeting = true
24 | end
25 |
26 | function P:action(bot, msg, group)
27 | local targets, output, reason = autils.targets(bot, msg)
28 | local hammered_users = anise.set()
29 |
30 | for target, _ in pairs(targets) do
31 | local name = utilities.lookup_name(bot, target)
32 |
33 | if autils.rank(bot, target, msg.chat.id) >= 4 then
34 | table.insert(output, name .. ' is an administrator.')
35 | elseif bot.database.userdata.hammered[target] then
36 | table.insert(output, name .. ' is already globally banned.')
37 | else
38 | if group then bindings.kickChatMember{
39 | chat_id = msg.chat.id,
40 | user_id = target
41 | } end
42 | bot.database.userdata.hammered[target] = reason or true
43 | table.insert(output, name .. ' has been globally banned.')
44 | hammered_users:add(target, reason or true)
45 | end
46 | end
47 |
48 | utilities.send_reply(msg, table.concat(output, '\n'), 'html')
49 | if #hammered_users > 0 then
50 | autils.log(bot, {
51 | -- Do not send the chat ID from PMs or private groups.
52 | chat_id = group and group.data.admin
53 | and not group.data.admin.flags.private and msg.chat.id,
54 | targets = hammered_users,
55 | action = 'Globally banned',
56 | source_user = msg.from,
57 | reason = reason
58 | })
59 | end
60 | end
61 |
62 | P.list = {
63 | name = 'hammered',
64 | title = 'Globally Banned Users',
65 | type = 'userdata',
66 | key = 'hammered'
67 | }
68 |
69 | return P
70 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/interactive_flags.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | interactive_flags.lua
3 | Toggle administrative flags interactively!
4 |
5 | Copyright 2019 topkecleon
6 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 | ]]--
8 |
9 | local bindings = require('extern.bindings')
10 | local utilities = require('otouto.utilities')
11 |
12 | local P = {}
13 |
14 | function P:init(bot)
15 | assert(bot.named_plugins['admin.flags'],
16 | self.name .. ' requires admin.flags!')
17 |
18 | self.command = 'flagint'
19 | self.doc = 'Enable or disable administrative flags.'
20 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
21 | :t('flags?int').table
22 |
23 | self.administration = true
24 | self.privilege = 3
25 |
26 | self.flags_plugin = bot.named_plugins['admin.flags']
27 | end
28 |
29 | function P:action(_, msg, group)
30 | local message = self:build_message(group)
31 | message.chat_id = msg.chat.id
32 | bindings.sendMessage(message)
33 | end
34 |
35 | function P:callback_action(bot, query)
36 | local group = utilities.group(bot, query.message.chat.id)
37 | local flag_name = utilities.get_word(query.data, 2)
38 | -- toggle flag
39 | if group.data.admin.flags[flag_name] then
40 | group.data.admin.flags[flag_name] = nil
41 | else
42 | group.data.admin.flags[flag_name] = true
43 | end
44 | -- build message update
45 | local message = self:build_message(group)
46 | message.chat_id = query.message.chat.id
47 | message.message_id = query.message.message_id
48 | bindings.editMessageText(message)
49 | end
50 |
51 | function P:build_message(group)
52 | local keyboard = utilities.keyboard('inline_keyboard'):row()
53 | local i = 0
54 | for flag_name in pairs(self.flags_plugin.flags) do
55 | local symbol = '❌ '
56 | if group.data.admin.flags[flag_name] then
57 | symbol = '✅ '
58 | end
59 | keyboard:button(symbol .. flag_name, 'callback_data', self.name .. ' ' .. flag_name)
60 | i = i + 1
61 | if i % 3 == 0 then
62 | keyboard:row()
63 | end
64 | end
65 |
66 | return {
67 | reply_markup = keyboard:serialize(),
68 | text = self.flags_plugin:list_flags(group.data.admin.flags),
69 | parse_mode = 'html'
70 | }
71 | end
72 |
73 | return P
74 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/kick.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | kick.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local anise = require('anise')
8 | local bindings = require('extern.bindings')
9 | local utilities = require('otouto.utilities')
10 | local autils = require('otouto.autils')
11 |
12 | local P = {}
13 |
14 | function P:init(bot)
15 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
16 | :t('kick', true).table
17 | self.command = 'kick'
18 | self.doc = "Removes a user or users from the group. A reason can be given \z
19 | on a new line. Example:\
20 | /kick @examplus 5554321\
21 | Bad jokes."
22 | self.privilege = 2
23 | self.administration = true
24 | self.targeting = true
25 | self.duration = true
26 | end
27 |
28 | function P:action(bot, msg, _group, _user)
29 | local targets, output, reason = autils.targets(bot, msg)
30 | local kicked_users = anise.set()
31 |
32 | for target, _ in pairs(targets) do
33 | local name = utilities.lookup_name(bot, target)
34 | if autils.rank(bot, target, msg.chat.id) >= 2 then
35 | table.insert(output, name .. ' is too privileged to be kicked.')
36 | else
37 | -- It isn't documented, but unbanChatMember also kicks.
38 | -- Thanks, Durov.
39 | local success, result = bindings.unbanChatMember{
40 | chat_id = msg.chat.id,
41 | user_id = target
42 | }
43 | if success then
44 | table.insert(output, name .. ' has been kicked.')
45 | kicked_users:add(target, reason or true)
46 | else
47 | table.insert(output, 'Error kicking ' .. name .. ': ' ..
48 | result.description)
49 | end
50 | end
51 | end
52 |
53 | utilities.send_reply(msg, table.concat(output, '\n'), 'html')
54 | if #kicked_users > 0 then
55 | autils.log(bot, {
56 | chat_id = msg.chat.id,
57 | targets = kicked_users,
58 | action = 'Kicked',
59 | source_user = msg.from,
60 | reason = reason
61 | })
62 | end
63 | end
64 |
65 | return P
66 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/kickme.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | kickme.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local bindings = require('extern.bindings')
8 | local utilities = require('otouto.utilities')
9 | local autils = require('otouto.autils')
10 |
11 | local P = {}
12 |
13 | function P:init(bot)
14 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
15 | :t('kickme').table
16 | self.command = 'kickme'
17 | self.doc = 'Removes the user from the group.'
18 | self.administration = true
19 | end
20 |
21 | function P:action(bot, msg)
22 | bindings.deleteMessage{
23 | chat_id = msg.chat.id,
24 | message_id = msg.message_id
25 | }
26 | if bindings.unbanChatMember{
27 | chat_id = msg.chat.id,
28 | user_id = msg.from.id
29 | } then
30 | autils.log(bot, {
31 | chat_id = msg.chat.id,
32 | target = msg.from.id,
33 | action = 'Kicked',
34 | source = self.name
35 | })
36 | end
37 | end
38 |
39 | return P
40 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/list_admins.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | list_admins.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 |
9 | local P = {}
10 |
11 | function P:init(bot)
12 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
13 | :t('admins'):t('list_?admins').table
14 | self.command = 'admins'
15 | self.doc = 'Returns a list of global administrators.'
16 | self.privilege = 2
17 | end
18 |
19 | function P:action(bot, msg, _group, _user)
20 | local admin_list =
21 | utilities.list_names(bot, bot.database.userdata.administrator)
22 | table.insert(admin_list, 1, 'Global administrators:')
23 | utilities.send_reply(msg, table.concat(admin_list, '\n• '), 'html')
24 | end
25 |
26 | return P
27 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/list_flags.fnl:
--------------------------------------------------------------------------------
1 | ; list_flags.fnl
2 | ; Copyright 2018 topkecleon
3 | ; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
4 |
5 | (require-macros :anise.macros)
6 | (require* otouto.utilities)
7 |
8 | {
9 | :init (fn [self bot]
10 | (set self.command "listflags")
11 | (set self.doc "Returns a list of enabled and disabled flags. Governors and\z
12 | administrators can use /flags to configure them.")
13 | (set self.triggers (utilities.make_triggers bot [] "list_?flags"))
14 | (set self.administration true)
15 | (values))
16 |
17 | :action (fn [_self bot msg group]
18 | (utilities.send_reply msg
19 | (: (. bot.named_plugins :admin.flags) :list_flags group.data.admin.flags)
20 | :html))
21 | }
22 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/list_groups.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | list_groups.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 | local bindings = require('extern.bindings')
9 | local plists
10 |
11 | local P = {}
12 |
13 | function P:init(bot)
14 | plists = bot.named_plugins['core.paged_lists']
15 | assert(plists, self.name .. ' requires core.paged_lists.')
16 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
17 | :t('groups?', true):t('list_?groups', true).table
18 | self.command = 'groups [query]'
19 | self.doc = "/groups [query]\
20 | Returns a list of all public, administrated groups, or the results of a query."
21 | end
22 |
23 | function P:action(bot, msg)
24 | local input = utilities.input_from_msg(msg)
25 | input = input and input:lower()
26 |
27 | -- Output will be a list of results, a list of all groups, or an explanation
28 | -- that there are no (listed) groups.
29 | local titles_and_links = {}
30 | for id_str, chat in pairs(bot.database.groupdata.admin) do
31 | if not chat.flags.private then
32 | table.insert(titles_and_links,
33 | bot.database.groupdata.info[id_str].title .. chat.link)
34 | end
35 | end
36 |
37 | local listed_groups = {}
38 | local results = {}
39 | table.sort(titles_and_links)
40 | for _, s in ipairs(titles_and_links) do
41 | local a, b = s:match('^(.+)(https://.-)$')
42 | local fmtd = string.format('%s', b, utilities.html_escape(a))
43 | table.insert(listed_groups, fmtd)
44 | if input and a:lower():find(input, 1, true) then
45 | table.insert(results, fmtd)
46 | end
47 | end
48 |
49 | local output
50 | if input then
51 | if #results == 0 then
52 | output = bot.config.errors.results
53 | else
54 | plists:send(bot, msg, results, 'Group Results', msg.chat.id)
55 | end
56 | elseif #listed_groups == 0 then
57 | output = 'There are no listed groups.'
58 | else
59 | local success, result = plists:send(bot, msg, listed_groups, 'Groups')
60 | if success then
61 | if result.result.chat.id ~= msg.chat.id then
62 | output = 'I have sent you the requested info privately.'
63 | else
64 | bindings.deleteMessage{
65 | chat_id = msg.chat.id,
66 | message_id = msg.message_id
67 | }
68 | end
69 | else
70 | output = 'Please message me privately first.'
72 | end
73 | end
74 | if output then utilities.send_reply(msg, output, 'html') end
75 | end
76 |
77 | return P
78 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/list_mods.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | list_mods.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 |
9 | local P = {}
10 |
11 | function P:init(bot)
12 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
13 | :t('mods'):t('ops'):t('list_?mods').table
14 | self.command = 'mods'
15 | self.doc = 'Returns a list of group moderators.'
16 | self.administration = true
17 | end
18 |
19 | function P:action(bot, msg, group)
20 | local admin = group.data.admin
21 | local mod_list = utilities.list_names(bot, admin.moderators)
22 | table.insert(mod_list, 1, '\n\nModerators:')
23 | local output = 'Governor: ' .. utilities.lookup_name(bot,
24 | admin.governor) .. table.concat(mod_list, '\n• ')
25 | utilities.send_reply(msg, output, 'html')
26 | end
27 |
28 | return P
29 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/list_rules.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | list_rules.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 |
9 | local P = {}
10 |
11 | function P:init(bot)
12 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
13 | :t('rules?', true):t('list_?rules').table
14 | self.command = 'rules [i]'
15 | self.doc = 'Returns the list of rules, or the specified rule.'
16 | self.administration = true
17 | end
18 |
19 | function P:action(_bot, msg, group)
20 | local admin = group.data.admin
21 | local input = tonumber(utilities.get_word(msg.text, 2))
22 | local output
23 | if #admin.rules == 0 then
24 | output = 'No rules have been set for this group.'
25 | elseif input and admin.rules[input] then
26 | output = self.rule_list(admin.rules)[input]
27 | else
28 | output = 'Rules for ' ..utilities.html_escape(msg.chat.title).. ':\n'
29 | .. table.concat(self.rule_list(admin.rules), '\n')
30 | end
31 | utilities.send_reply(msg.reply_to_message or msg, output, 'html')
32 | end
33 |
34 | function P.rule_list(rules)
35 | local t = {}
36 | for i, rule in ipairs(rules) do
37 | table.insert(t, '' .. i .. '. ' .. rule)
38 | end
39 | return t
40 | end
41 |
42 | return P
43 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/mute.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | mute.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local anise = require('anise')
8 | local bindings = require('extern.bindings')
9 | local utilities = require('otouto.utilities')
10 | local autils = require('otouto.autils')
11 |
12 | local P = {}
13 |
14 | function P:init(bot)
15 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
16 | :t('mute', true).table
17 | self.command = 'mute'
18 | self.doc = "Mute a user or users indefinitely or for the time specified. \z
19 | A duration in the tiem format (see /help tiem) can be given before the reason. \
20 | Examples:\
21 | /mute @foo @bar 8675309\
22 | 2h30m No cursing on my Christian server.\
23 | \
24 | [in reply] /mute 240"
25 | self.privilege = 2
26 | self.administration = true
27 | self.targeting = true
28 | self.duration = true
29 | end
30 |
31 | function P:action(bot, msg, _group, _user)
32 | local targets, output, reason, duration =
33 | autils.targets(bot, msg, {get_duration = true})
34 | local muted_users = anise.set()
35 |
36 | -- Durations shorter than 30 seconds and longer than a leap year are
37 | -- interpreted as "forever" by the bot API.
38 | if duration and (duration > 366*24*60*60 or duration < 60) then
39 | duration = nil
40 | table.insert(output,
41 | 'Durations must be longer than a minute and shorter than a year.')
42 | end
43 |
44 | local out_str, log_str
45 | if duration then
46 | out_str = ' has been muted for ' ..
47 | utilities.tiem.print(duration) .. '.'
48 | log_str = 'Muted for ' .. utilities.tiem.print(duration)
49 | else
50 | out_str = ' has been muted.'
51 | log_str = 'Muted'
52 | end
53 |
54 | for target, _ in pairs(targets) do
55 | local name = utilities.lookup_name(bot, target)
56 |
57 | if autils.rank(bot, target, msg.chat.id) >= 2 then
58 | table.insert(output,name .. ' is too privileged to be muted.')
59 | else
60 | local success, result = bindings.restrictChatMember{
61 | chat_id = msg.chat.id,
62 | user_id = target,
63 | until_date = duration and os.time() + duration,
64 | can_send_messages = false
65 | }
66 | if success then
67 | table.insert(output, name .. out_str)
68 | muted_users:add(target, reason or true)
69 | else
70 | table.insert(output, result.description .. ' (' ..target.. ')')
71 | end
72 | end
73 | end
74 |
75 | utilities.send_reply(msg, table.concat(output, '\n'), 'html')
76 | if #muted_users > 0 then
77 | autils.log(bot, {
78 | chat_id = msg.chat.id,
79 | targets = muted_users,
80 | action = log_str,
81 | source_user = msg.from,
82 | reason = reason
83 | })
84 | end
85 | end
86 |
87 | return P
88 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/regen_link.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | regen_link.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local bindings = require('extern.bindings')
8 | local utilities = require('otouto.utilities')
9 |
10 | local P = {}
11 |
12 | function P:init(bot)
13 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
14 | :t('regen_?link').table
15 | self.command = 'regenlink'
16 | self.doc = 'Regenerates the group link.'
17 | self.administration = true
18 | self.privilege = 2
19 | end
20 |
21 | function P:action(_bot, msg, group)
22 | local success, result = bindings.exportChatInviteLink{chat_id = msg.chat.id}
23 | if success then
24 | group.data.admin.link = result.result
25 | utilities.send_reply(msg, 'The link has been regenerated.')
26 | else
27 | utilities.send_reply(msg, result.description)
28 | end
29 | end
30 |
31 | return P
32 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/remove_group.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | remove_group.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 |
9 | local P = {}
10 |
11 | function P:init(bot)
12 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
13 | :t('remove_?group', true):t('rem_?group').table
14 | self.command = 'removegroup [chat ID]'
15 | self.doc = "/removegroup [chat ID]\
16 | Removes the current or specified group from the administrative system."
17 | self.privilege = 4
18 | end
19 |
20 | function P:action(bot, msg, group)
21 | local input = utilities.get_word(msg.text, 2)
22 | local output
23 |
24 | if input then
25 | local id = tostring(tonumber(input))
26 | if id then
27 | local admin = bot.database.groupdata.admin
28 | if admin[id] then
29 | output = 'I am no longer administrating ' .. admin[id].name .. '.'
30 | admin[id] = nil
31 | elseif admin['-100'..id] then
32 | output = 'I am no longer administrating ' .. admin['-100'..id].name .. '.'
33 | admin['-100'..id] = nil
34 | else
35 | output = 'Group not found (' .. id .. ').'
36 | end
37 | else
38 | output = 'Input must be a group ID.'
39 | end
40 |
41 | elseif group and group.data.admin then
42 | output = 'I am no longer administrating ' .. msg.chat.title .. '.'
43 | group.data.admin = nil
44 | else
45 | output = 'Run in an administrated group or pass one\'s ID.'
46 | end
47 |
48 | utilities.send_reply(msg, output)
49 |
50 | end
51 |
52 | return P
53 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/set_description.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | set_description.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 |
9 | local P = {}
10 |
11 | function P:init(bot)
12 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
13 | :t('set_?description', true):t('set_?desc', true).table
14 | self.command = 'setdesc '
15 | self.doc = 'Set a group description. Passing "--" will delete the current one.'
16 | self.privilege = 3
17 | self.administration = true
18 | end
19 |
20 | function P:action(_bot, msg, group)
21 | local admin = group.data.admin
22 | local input = utilities.input_from_msg(msg)
23 | if not input then
24 | if admin.description then
25 | utilities.send_reply(msg, 'Current description:\n' ..
26 | admin.description, 'html')
27 | else
28 | utilities.send_reply(msg, 'This group has no description.')
29 | end
30 | elseif input == '--' or input == utilities.char.em_dash then
31 | admin.description = nil
32 | utilities.send_reply(msg, 'The group description has been cleared.')
33 | else
34 | admin.description = input
35 | utilities.send_reply(msg, 'Description updated:\n' ..
36 | admin.description, 'html')
37 | end
38 | end
39 |
40 | return P
41 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/set_governor.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | set_governor.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 | local autils = require('otouto.autils')
9 |
10 | local P = {}
11 |
12 | function P:init(bot)
13 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
14 | :t('governor', true):t('gov', true).table
15 | self.command = 'governor '
16 | self.doc = 'Set the group\'s governor.'
17 | self.privilege = 3
18 | self.administration = true
19 | end
20 |
21 | function P:action(bot, msg, group)
22 | local targets, errors = autils.targets(bot, msg, {unknown_ids_err = true})
23 | local output
24 |
25 | if #targets == 1 then
26 | local target = targets:next()
27 | local admin = group.data.admin
28 | local name = utilities.lookup_name(bot, target)
29 | autils.promote_admin(msg.chat.id, target, true)
30 |
31 | if target == admin.governor then
32 | output = name .. ' is already governor.'
33 |
34 | else
35 | -- Demote the old governor if he's not an admin.
36 | if autils.rank(bot, admin.governor, msg.chat.id) < 4 then
37 | autils.demote_admin(msg.chat.id, admin.governor)
38 | end
39 |
40 | admin.moderators[target] = nil
41 | admin.bans[target] = nil
42 | admin.governor = target
43 | output = name .. ' is now governor.'
44 | end
45 |
46 | elseif #targets == 0 then
47 | output = bot.config.errors.specify_target
48 |
49 | else -- multiple targets
50 | output = 'Please only specify one new governor.'
51 | end
52 |
53 | utilities.send_reply(msg, output .. table.concat(errors, '\n'), 'html')
54 | end
55 |
56 | return P
57 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/set_rules.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | set_rules.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local utilities = require('otouto.utilities')
8 |
9 | local P = {}
10 |
11 | function P:init(bot)
12 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
13 | :t('set_?rules?', true):t('change_?rules?', true)
14 | :t('add_?rules?', true):t('del_?rules?', true).table
15 | self.command = 'setrules '
16 | -- luacheck: push no max string line length
17 | self.doc = [[
18 | change [i]
19 | Changes an existing rule or rules, starting at $i. If $i is unspecified, all rules will be overwritten.
20 | Alias: /changerule
21 |
22 | add [i]
23 | Adds a new rule or rules, inserted starting at $i. If $i is unspecified, the new rule or rules will be added to the end of the list.
24 | Alias: /addrule
25 |
26 | del [i]
27 | Deletes a rule. If $i is unspecified, all rules will be deleted.
28 | Alias: /delrule
29 |
30 | Examples:
31 | • Set all the rules for a group.
32 | /changerules
33 | First rule.
34 | Second rule.
35 | ...
36 |
37 | • Change rule 4.
38 | /changerule 4
39 | Changed fourth rule.
40 | [Changed fifth rule.]
41 |
42 | • Add a rule or rules between 2 and 3.
43 | /addrule 3
44 | New third rule.
45 | [New fourth rule.]
46 | ]]
47 | -- luacheck: pop
48 |
49 | self.privilege = 3
50 | self.administration = true
51 | end
52 |
53 | function P:action(bot, msg, group)
54 | local subc, idx
55 |
56 | -- "/changerule ..." -> "/setrules change ..."
57 | local c = '^' .. bot.config.cmd_pat
58 | if msg.text_lower:match(c..'change_?rule') then
59 | subc = 'change'
60 | idx = msg.text_lower:match(c..'change_?rules?%s+(%d+)')
61 |
62 | elseif msg.text_lower:match(c..'add_?rule') then
63 | subc = 'add'
64 | idx = msg.text_lower:match(c..'add_?rules?%s+(%d+)')
65 |
66 | elseif msg.text_lower:match(c..'del_?rule') then
67 | subc = 'del'
68 | idx = msg.text_lower:match(c..'del_?rules?%s+(%d+)')
69 | else
70 | subc, idx = msg.text_lower:match(c..'set_?rules?%s+(%a+)%s*(%d*)')
71 | end
72 |
73 | local nrules = msg.text:match('^.-\n+(.+)$') or msg.reply_to_message and
74 | msg.reply_to_message.text
75 | local new_rules = {}
76 | if nrules then
77 | for s in string.gmatch(nrules..'\n', '(.-)\n') do
78 | table.insert(new_rules, s)
79 | end
80 | end
81 |
82 | local output
83 | if self.subcommands[subc] then
84 | output = self.subcommands[subc](self, group, new_rules, tonumber(idx))
85 | else
86 | output = 'Invalid subcommand. See /help setrules.'
87 | end
88 |
89 | utilities.send_reply(msg, output, 'html')
90 | end
91 |
92 | P.subcommands = {
93 | change = function (super, group, new_rules, idx)
94 | local admin = group.data.admin
95 | if #new_rules == 0 then
96 | return 'Please specify the new rule or rules.'
97 |
98 | elseif not idx then -- /setrules
99 | admin.rules = new_rules
100 | local output = 'Rules for ' .. utilities.html_escape(
101 | group.data.info.title) .. ':'
102 | for i, rule in ipairs(admin.rules) do
103 | output = output .. '\n' .. i .. '. ' .. rule
104 | end
105 | return output
106 |
107 | elseif idx < 1 then
108 | return 'Invalid index.'
109 |
110 | elseif idx > #admin.rules then
111 | return super.subcommands.add(_, group, new_rules, idx)
112 |
113 | else -- /changerule i
114 | local output = ''
115 | for i = 1, #new_rules do
116 | admin.rules[idx+i-1] = new_rules[i]
117 | output = output .. '\n' .. idx+i-1 .. '. ' .. new_rules[i]
118 | end
119 | return output
120 | end
121 | end,
122 |
123 | add = function (_super, group, new_rules, idx)
124 | local admin = group.data.admin
125 | if #new_rules == 0 then
126 | return 'Please specify the new rule or rules.'
127 |
128 | elseif not idx or idx > #admin.rules then -- /addrule
129 | local output = ''
130 | for i = 1, #new_rules do
131 | table.insert(admin.rules, new_rules[i])
132 | output = output .. '\n' .. #admin.rules .. '. ' .. new_rules[i]
133 | end
134 | return output
135 |
136 | elseif idx < 1 then
137 | return 'Invalid index.'
138 |
139 | else -- /addrule i
140 | local output = ''
141 | for i = 1, #new_rules do
142 | table.insert(admin.rules, idx+i-1, new_rules[i])
143 | output = output .. '\n' .. idx+i-1 .. '. ' .. new_rules[i]
144 | end
145 | return output
146 | end
147 | end,
148 |
149 | del = function (_super, group, _new_rules, idx)
150 | local admin = group.data.admin
151 | if not idx then -- /setrules --
152 | admin.rules = {}
153 | return 'The rules have been deleted.'
154 |
155 | elseif idx > #admin.rules or idx < 0 then
156 | return 'Invalid index.'
157 |
158 | else -- /changerule i --
159 | table.remove(admin.rules, idx)
160 | return 'Rule ' .. idx .. ' has been deleted.'
161 | end
162 | end,
163 | }
164 |
165 | return P
166 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/temp_ban.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | temp_ban.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local anise = require('anise')
8 | local bindings = require('extern.bindings')
9 | local utilities = require('otouto.utilities')
10 | local autils = require('otouto.autils')
11 |
12 | local P = {}
13 |
14 | function P:init(bot)
15 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
16 | :t('temp_?ban', true).table
17 | self.command = 'tempban'
18 | self.doc = "Bans a user or users. A duration must be given on a new line \z
19 | (or after the command in a reply). A reason can be given after that. The \z
20 | duration must be an interval in the tiem format (see /help tiem). Example:\
21 | /tempban @examplus 5554321\
22 | 3d12h Bad jokes."
23 | self.privilege = 2
24 | self.administration = true
25 | self.targeting = true
26 | self.duration = true
27 | end
28 |
29 | function P:action(bot, msg, _group, _user)
30 | local targets, output, reason, duration =
31 | autils.targets(bot, msg, {get_duration = true})
32 | local banned_users = anise.set()
33 |
34 | if not duration or duration > 366*24*60*60 or duration < 60 then
35 | table.insert(output,
36 | 'Durations must be longer than a minute and shorter than a year.')
37 | else
38 | for target, _ in pairs(targets) do
39 | local name = utilities.lookup_name(bot, target)
40 | if autils.rank(bot, target, msg.chat.id) > 2 then
41 | table.insert(output, name .. ' is too privileged to be banned.')
42 | else
43 | local success, result = bindings.kickChatMember{
44 | chat_id = msg.chat.id,
45 | user_id = target,
46 | until_date = duration + os.time()
47 | }
48 | if success then
49 | table.insert(output, name .. ' has been banned for ' ..
50 | utilities.tiem.print(duration) .. '.')
51 | banned_users:add(target, reason or true)
52 | else
53 | table.insert(output, 'Error banning ' .. name .. ': ' ..
54 | result.description)
55 | end
56 | end
57 | end
58 | end
59 |
60 | utilities.send_reply(msg, table.concat(output, '\n'), 'html')
61 | if #banned_users > 0 then
62 | autils.log(bot, {
63 | chat_id = msg.chat.id,
64 | targets = banned_users,
65 | action = 'Banned for '..utilities.tiem.print(duration),
66 | source_user = msg.from,
67 | reason = reason
68 | })
69 | end
70 | end
71 |
72 | return P
73 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/unhammer.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | unhammer.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local anise = require('anise')
8 | local utilities = require('otouto.utilities')
9 | local autils = require('otouto.autils')
10 |
11 | local P = {}
12 |
13 | function P:init(bot)
14 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
15 | :t('unhammer', true).table
16 | self.command = 'unhammer'
17 | self.privilege = 4
18 | self.targeting = true
19 | end
20 |
21 | function P:action(bot, msg, group)
22 | local targets, output, reason = autils.targets(bot, msg)
23 | local unhammered_users = anise.set()
24 |
25 | for target, _ in pairs(targets) do
26 | local user = utilities.user(bot, target)
27 | -- Reset the global antilink counter.
28 | user.data.antilink = nil
29 | if user.data.hammered then
30 | user.data.hammered = nil
31 | unhammered_users:add(target, reason or true)
32 | table.insert(output, user:name() .. ' is no longer globally banned.')
33 | else
34 | table.insert(output, user:name() .. ' is not globally banned.')
35 | end
36 | end
37 |
38 | utilities.send_reply(msg, table.concat(output, '\n'), 'html')
39 | if #unhammered_users > 0 then
40 | autils.log(bot, {
41 | -- Do not send the chat ID from PMs or private groups.
42 | chat_id = group and group.data.admin
43 | and not group.data.admin.flags.private and msg.chat.id,
44 | targets = unhammered_users,
45 | action = "Unhammered",
46 | source_user = msg.from,
47 | reason = reason
48 | })
49 | end
50 | end
51 |
52 | return P
53 |
--------------------------------------------------------------------------------
/otouto/plugins/admin/unrestrict.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | unrestrict.lua
3 | Copyright 2018 topkecleon
4 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
5 | ]]--
6 |
7 | local bindings = require('extern.bindings')
8 | local utilities = require('otouto.utilities')
9 | local autils = require('otouto.autils')
10 |
11 | local P = {}
12 |
13 | function P:init(bot)
14 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
15 | :t('unrestrict', true):t('unmute', true):t('unban', true).table
16 | self.command = 'unrestrict'
17 | self.doc = "Unban a target or targets. Each target is removed from the \z
18 | group's ban list and unrestricted on the Telegram level. Globally banned \z
19 | users will still be unable to access the group (see /help antihammer). \z
20 | \nAliases: /unmute, /unban."
21 | self.doc = self.doc:gsub('/', bot.config.cmd_pat)
22 | self.privilege = 2
23 | self.administration = true
24 | self.targeting = true
25 | end
26 |
27 | function P:action(bot, msg, group)
28 | local targets, output = autils.targets(bot, msg)
29 | for target, _ in pairs(targets) do
30 | local name = utilities.lookup_name(bot, target)
31 | bindings.restrictChatMember{
32 | chat_id = msg.chat.id,
33 | user_id = target,
34 | can_send_other_messages = true,
35 | can_add_web_page_previews = true
36 | }
37 | local admin = group.data.admin
38 | admin.strikes[target] = nil
39 | if admin.bans[target] then
40 | admin.bans[target] = nil
41 | table.insert(output, name ..
42 | ' has been unbanned and unrestricted.')
43 | else
44 | table.insert(output, name .. ' has been unrestricted.')
45 | end
46 | if bot.database.userdata.hammered[target] then
47 | table.insert(output, name ..
48 | (' is globally banned. See %shelp antihammer.')
49 | :format(bot.config.cmd_pat))
50 | end
51 | end
52 | utilities.send_reply(msg, table.concat(output, '\n'), 'html')
53 | end
54 |
55 | return P
56 |
--------------------------------------------------------------------------------
/otouto/plugins/core/about.fnl:
--------------------------------------------------------------------------------
1 | ;; about.fnl
2 | ;; Returns owner-configured information related to the bot and a link to the
3 | ;; source code.
4 |
5 | ;; Copyright 2018 topkecleon
6 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 |
8 | (require-macros :anise.macros)
9 | (require* otouto.utilities)
10 |
11 | {
12 | :init (fn [self bot]
13 | (set self.command "about")
14 | (set self.doc "Returns information about the bot")
15 | (set self.text (f-str "{bot.config.about_text}\z
16 | \nBased on otouto v{bot.version} by topkecleon."))
17 | (set self.triggers (utilities.make_triggers bot [] :about :start))
18 | nil)
19 |
20 | :action (fn [self, bot, msg]
21 | (utilities.send_message msg.chat.id self.text true nil :html)
22 | (values))
23 | }
24 |
--------------------------------------------------------------------------------
/otouto/plugins/core/control.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | control.lua
3 | Provides various commands to manage the bot.
4 |
5 | /reload [-config]
6 | Reloads the bot, optionally without reloading config.
7 |
8 | /halt
9 | Safely stops the bot.
10 |
11 | /do
12 | Runs multiple, newline-separated commands as if they were individual
13 | messages.
14 |
15 | Copyright 2016 topkecleon
16 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
17 | ]]--
18 |
19 | local anise = require('anise')
20 |
21 | local utilities = require('otouto.utilities')
22 |
23 | local control = {}
24 |
25 | function control:init(bot)
26 | local cmd_pat = bot.config.cmd_pat
27 | self.cmd_pat = cmd_pat
28 | self.triggers = utilities.triggers(bot.info.username, cmd_pat,
29 | {'^'..cmd_pat..'do'}):t('hotswap', true):t('reload', true):t('halt').table
30 |
31 | bot.database.control = bot.database.control or {}
32 | -- Ability to send the bot owner a message after a successful restart.
33 | if bot.database.control.on_start then
34 | utilities.send_message(bot.database.control.on_start.id, bot.database.control.on_start.text)
35 | bot.database.control.on_start = nil
36 | end
37 | end
38 |
39 | function control:action(bot, msg)
40 |
41 | if msg.from.id ~= bot.config.admin then
42 | return
43 | end
44 |
45 | if msg.date < os.time() - 2 then return end
46 |
47 | local cmd_pat = self.cmd_pat
48 | if msg.text_lower:match('^'..cmd_pat..'hotswap') then
49 | local errs = {}
50 | local init = false
51 | for modname in anise.split_str_iter(utilities.input(msg.text)) do
52 | if modname == '!' then
53 | init = true
54 | else
55 | local mod, err = anise.hotswap(modname)
56 | if err ~= nil then
57 | table.insert(errs, err)
58 | end
59 | if init then
60 | mod:init(bot)
61 | end
62 | end
63 | end
64 | local reply = "Modules reloaded!"
65 | if #errs ~= 0 then
66 | reply = reply .. '\nErrors:\n' .. table.concat(errs, '\n')
67 | end
68 | utilities.send_reply(msg, reply)
69 | elseif msg.text_lower:match('^'..cmd_pat..'reload') then
70 | for pac, _ in pairs(package.loaded) do
71 | if pac:match('^otouto%.plugins%.') then
72 | package.loaded[pac] = nil
73 | end
74 | end
75 | package.loaded['extern.bindings'] = nil
76 | package.loaded['otouto.utilities'] = nil
77 | package.loaded['anise'] = nil
78 | package.loaded['otouto.autils'] = nil
79 | if not msg.text_lower:match('%-config') then
80 | package.loaded['config'] = nil
81 | bot.config = require('config')
82 | end
83 | bot.database.control.on_start = {
84 | id = msg.chat.id,
85 | text = 'Bot reloaded!'
86 | }
87 | bot:init()
88 | elseif msg.text_lower:match('^'..cmd_pat..'halt') then
89 | bot.is_started = false
90 | utilities.send_reply(msg, 'Stopping bot!')
91 | bot.database.control.on_start = {
92 | id = msg.chat.id,
93 | text = 'Bot started!'
94 | }
95 | elseif msg.text_lower:match('^'..cmd_pat..'do') then
96 | local input = msg.text_lower:match('^'..cmd_pat..'do\n(.+)')
97 | if not input then
98 | utilities.send_reply(msg, 'usage: ```\n'..cmd_pat..'do\n'..cmd_pat..'command \n...\n```', true)
99 | return
100 | end
101 | for command in (input..'\n'):gmatch('(.-)\n+') do
102 | command = anise.trim(command)
103 | msg.text = command
104 | bot:on_message(msg)
105 | end
106 | end
107 |
108 | end
109 |
110 | return control
111 |
--------------------------------------------------------------------------------
/otouto/plugins/core/delete_messages.fnl:
--------------------------------------------------------------------------------
1 | ;; delete_messages.fnl
2 | ;; Provides a "later" job to delete messages.
3 |
4 | (local bindings (require "extern.bindings"))
5 |
6 | {
7 | :later (fn [_self _bot param]
8 | (bindings.deleteMessage {
9 | :chat_id param.chat_id
10 | :message_id param.message_id}))
11 | }
12 |
13 | ;; (: bot :do_later :core.delete_messages when {
14 | ;; :chat_id msg.chat.id
15 | ;; :message_id msg.message_id})
16 |
--------------------------------------------------------------------------------
/otouto/plugins/core/disable_plugins.fnl:
--------------------------------------------------------------------------------
1 | ;; disable_plugins.fnl
2 | ;; This plugin manages the list of disabled plugins for a group. Put this
3 | ;; anywhere in the plugin ordering.
4 |
5 | ;; Copyright 2017 bb010g
6 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 |
8 | (require-macros :anise.macros)
9 | (require* anise
10 | extern.bindings
11 | otouto.utilities)
12 |
13 | {
14 | :init (fn [self bot]
15 | (set self.command "(disable|enable) …")
16 | (set self.doc "Sets whether plugins are enabled or disabled in a group. You must have ban \z
17 | permissions to use this.\n\n\z
18 | If no plugins are provided, currently disabled plugins are listed.")
19 | (set self.triggers (utilities.make_triggers bot [] [:disable true] [:enable true]))
20 | (values))
21 |
22 | :get_disabled (fn [disabled chat_str create]
23 | (var chat_disabled (. disabled chat_str))
24 | (when (= chat_disabled nil)
25 | (set chat_disabled {})
26 | (when create (tset disabled chat_str chat_disabled)))
27 | chat_disabled)
28 |
29 | :blacklist {
30 | :core.about true
31 | :core.control true
32 | :core.delete_messages true
33 | :core.disable_plugins true
34 | :core.end_forwards true
35 | :core.group_info true
36 | :core.group_whitelist true
37 | :core.luarun true
38 | :core.paged_lists true
39 | :core.user_blacklist true
40 | :core.user_info true
41 | :core.user_lists true
42 | }
43 |
44 | :toggle (fn [self named_plugins chat_disabled enable pnames]
45 | (let [blacklist self.blacklist
46 | disabled {}
47 | enabled {}
48 | not_found {}
49 | blacklisted {}]
50 | (each [_ pname (pairs pnames)]
51 | (if (not (. named_plugins pname))
52 | (table.insert not_found pname)
53 | (. blacklist pname)
54 | (table.insert blacklisted pname)
55 | (and enable (. chat_disabled pname))
56 | (do (tset chat_disabled pname nil)
57 | (table.insert enabled pname))
58 | (and (not enable) (not (. chat_disabled pname)))
59 | (do (tset chat_disabled pname true)
60 | (table.insert disabled pname))
61 | ; else
62 | nil))
63 | (values disabled enabled not_found blacklisted)))
64 |
65 | :action (fn [self bot msg]
66 | (local chat_id msg.chat.id)
67 | (local chat_str (tostring chat_id))
68 | (local input (utilities.input_from_msg msg))
69 | (local disabled_plugins bot.database.disabled_plugins)
70 | (if (not input)
71 | (let [chat_disabled (self.get_disabled disabled_plugins chat_str false)
72 | disabled (anise.keys chat_disabled)]
73 | (if (not (. disabled 1))
74 | (do (utilities.send_message chat_id "All plugins are enabled.") nil)
75 | (let [output (.. "Disabled plugins:\n• " (table.concat disabled "\n• "))]
76 | (utilities.send_message chat_id output true nil :html)
77 | nil)))
78 | (let [chat_disabled (self.get_disabled disabled_plugins chat_str true)
79 | (cm_success chat_member) (bindings.getChatMember {:chat_id chat_id :user_id msg.from.id})
80 | chat_member (and cm_success chat_member.result)]
81 | (if
82 | (not cm_success)
83 | (do (utilities.send_reply msg "Couldn't fetch permissions.") nil)
84 | (not (or chat_member.can_restrict_members (= chat_member.status :creator)))
85 | (do (utilities.send_reply msg "You need ban permissions.") nil)
86 | ; else
87 | (let [enable
88 | (and-or (: msg.text_lower :match (f-str "^{bot.config.cmd_pat}enable")) true false)
89 | pnames (anise.split_str input)
90 | (disabled enabled not_found blacklisted)
91 | (: self :toggle bot.named_plugins chat_disabled enable pnames)
92 | output {}
93 | sep ", "]
94 | (when (= (next chat_disabled) nil)
95 | (tset disabled_plugins chat_str nil))
96 | (table.insert output
97 | (if (. blacklisted 1)
98 | (.. "Blacklisted: " (table.concat blacklisted sep))
99 | (. disabled 1)
100 | (.. "Disabled: " (table.concat disabled sep))
101 | (. enabled 1)
102 | (.. "Enabled: " (table.concat enabled sep))
103 | (. not_found 1)
104 | (.. "Not found: " (table.concat not_found sep))
105 | ; else
106 | "Nothing changed."))
107 | (utilities.send_reply msg (table.concat output "\n") :html)
108 | nil)))))
109 | }
110 |
--------------------------------------------------------------------------------
/otouto/plugins/core/end_forwards.fnl:
--------------------------------------------------------------------------------
1 | ;; end_forwards.fnl
2 | ;; This plugin keeps forwarded messages from hitting any plugin after it in the
3 | ;; load order. Just put this wherever, close to the top. The only plugins which
4 | ;; need to see forwarded messages are usually administration-related.
5 |
6 | ;; Copyright 2016 topkecleon
7 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
8 |
9 | { :triggers [""] :action (fn [_ _ msg] (and (not msg.forward_from) :continue)) }
10 |
--------------------------------------------------------------------------------
/otouto/plugins/core/group_info.fnl:
--------------------------------------------------------------------------------
1 | ; group_info.fnl
2 | ; Stores group info in database.groupdata.info.
3 | ; Also logs changes of the names of administrated groups.
4 |
5 | ; Copyright 2018 topkecleon
6 | ; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 |
8 | (require-macros :anise.macros)
9 | (require* otouto.autils)
10 |
11 | {
12 | :triggers [""]
13 | :error false
14 |
15 | :init (fn [_ bot] (tset? bot.database.groupdata :info {}))
16 |
17 | :action (fn [_ bot msg group]
18 | (when group (if (and group.data.admin
19 | (not group.data.admin.flags.private)
20 | group.data.info
21 | (~= msg.chat.title group.data.info.title))
22 | (autils.log bot {
23 | :chat_id msg.chat.id
24 | :action "Title changed"
25 | :reason msg.chat.title
26 | :source_user (if msg.new_chat_title msg.from)}))
27 | (set group.data.info msg.chat))
28 | :continue)
29 |
30 | :list {
31 | :name :groups
32 | :title "Known Groups"
33 | :type :groupdata
34 | :key :info
35 | :sudo true
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/otouto/plugins/core/group_whitelist.fnl:
--------------------------------------------------------------------------------
1 | ; group_whitelist.fnl
2 | ; Basic whitelisting for groups. The group is whitelisted when the bot is added
3 | ; by one of its administrators. The group is unwhitelisted when the bot is
4 | ; removed. The bot will leave non-whitelisted groups.
5 |
6 | ; Copyright 2018 topkecleon
7 | ; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
8 |
9 | (require-macros :anise.macros)
10 | (require* extern.bindings)
11 |
12 | {
13 | :init (fn [_ bot] (set? bot.database.groupdata.whitelisted {}))
14 | :triggers [""]
15 | :error false
16 |
17 | :action (fn [_ bot msg group user]
18 | (if (not group) :continue
19 | ;else
20 | (do (if (and msg.left_chat_member (= msg.left_chat_member.id bot.info.id))
21 | (set group.data.whitelisted nil)
22 | (and msg.new_chat_member
23 | (= msg.new_chat_member.id bot.info.id)
24 | (> (: user :rank bot) 3))
25 | (set group.data.whitelisted true))
26 | (if (or group.data.whitelisted group.data.admin)
27 | :continue
28 | (bindings.leaveChat {:chat_id msg.chat.id})))))
29 |
30 | :list {
31 | :name :whitelist
32 | :title "Whitelisted Groups"
33 | :type :groupdata
34 | :key :whitelisted
35 | :sudo true
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/otouto/plugins/core/help.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | help.lua
3 | Returns a list of commands, or command-specific help.
4 |
5 | Copyright 2016 topkecleon
6 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 | ]]--
8 |
9 | local utilities = require('otouto.utilities')
10 | local autils = require('otouto.autils')
11 |
12 | local help = {}
13 |
14 | function help:init(bot)
15 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
16 | :t('help', true):t('h', true).table
17 | self.command = 'help [command]'
18 | self.doc = 'Returns usage information for a given command.'
19 |
20 | self.glossaries = {}
21 | for name, glossary in pairs({
22 | autils = autils and autils.glossary,
23 | }) do
24 | if glossary then self.glossaries[name] = glossary end
25 | end
26 |
27 | self.commandlist = {}
28 | self:generate_text(bot.config.cmd_pat)
29 |
30 | self.proposition = 'Please message me privately for a list of commands.'
32 | end
33 |
34 | function help:action(bot, msg)
35 | local input = utilities.input(msg.text_lower)
36 | if input then
37 | input = input:lower():gsub('^' .. bot.config.cmd_pat, '')
38 | for _, plugin in ipairs(bot.plugins) do
39 | if plugin.help_word and input:match(plugin.help_word) then
40 | utilities.send_plugin_help(msg.chat.id, nil, bot.config.cmd_pat, plugin)
41 | return
42 | end
43 | end
44 | -- If there are no plugin matches, check the glossaries.
45 | for _glossary_name, glossary in pairs(self.glossaries) do
46 | for name, entry in pairs(glossary) do
47 | if input:match(name) then
48 | utilities.send_help_for(msg.chat.id, nil, name, entry)
49 | return
50 | end
51 | end
52 | end
53 | utilities.send_reply(msg, 'Sorry, there is no help for that command.')
54 |
55 | elseif not utilities.send_message(msg.from.id, self.text, true, nil, 'html') then
56 | -- Attempt to send the help message via PM.
57 | -- If msg is from a group, tell the group whether the PM was successful.
58 | utilities.send_reply(msg, self.proposition, 'html')
59 |
60 | elseif msg.chat.type ~= 'private' then
61 | utilities.send_reply(msg,
62 | 'I have sent you the requested information in a private message.')
63 | end
64 | end
65 |
66 | function help:on_plugins_load(bot, plugins)
67 | for _, plugin in pairs(plugins) do
68 | if plugin.command then
69 | local s = plugin.command
70 | if plugin.targeting then
71 | s = s .. '*'
72 | if plugin.duration then
73 | s = s .. '†'
74 | end
75 | end
76 | table.insert(self.commandlist, {plugin.name, s})
77 | if plugin.doc and not plugin.help_word then
78 | plugin.help_word = '^' .. utilities.get_word(plugin.command, 1)
79 | .. '$'
80 | end
81 | end
82 | end
83 | table.sort(self.commandlist, function (a, b) return a[2] < b[2] end)
84 | self:generate_text(bot.config.cmd_pat)
85 | end
86 |
87 | function help:on_plugins_unload(bot, plugins)
88 | for _, plugin in pairs(plugins) do
89 | for i, pair in ipairs(self.commandlist) do
90 | if pair[1] == plugin.name then
91 | table.remove(self.commandlist, i)
92 | break
93 | end
94 | end
95 | end
96 | self:generate_text(bot.config.cmd_pat)
97 | end
98 |
99 | function help:generate_text(cmd_pat)
100 | local comlist = '\n'
101 | for _, pair in ipairs(self.commandlist) do
102 | comlist = comlist .. '• ' .. cmd_pat .. pair[2] .. '\n'
103 | end
104 | comlist = comlist ..
105 | "Arguments: [optional]\
106 | * Targets may be specified via reply, username, mention, or ID. \z
107 | In a reply command, a reason can be given after the command. Otherwise, it must be on a new line.\
108 | † A duration may be specified before the reason, in seconds or in the format 5d12h30m15s."
109 | self.text = 'Available commands:' .. utilities.html_escape(comlist)
110 | end
111 |
112 | return help
113 |
--------------------------------------------------------------------------------
/otouto/plugins/core/luarun.fnl:
--------------------------------------------------------------------------------
1 | ;; luarun.lua
2 | ;; Allows the bot owner to run arbitrary Lua or Fennel code inside the bot instance.
3 | ;; "/return" is an alias for "/lua return".
4 |
5 | ;; Copyright 2016 topkecleon
6 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 |
8 | (require-macros :anise.macros)
9 | (require* otouto.utilities
10 | fennel
11 | fennelview
12 | serpent
13 | socket.url)
14 |
15 | {
16 | :init (fn [self bot]
17 | (set self.triggers (utilities.make_triggers bot [] [:lua true] [:fnl true] [:return true]))
18 | (set self.err_msg (fn [x] (.. "Error:\n" (tostring x))))
19 | (values))
20 |
21 | :fennel_preamble "\z
22 | (require-macros :anise.macros)\
23 | (require* anise\
24 | otouto.autils\
25 | extern.bindings\
26 | otouto.utilities\
27 | fennel\
28 | fennelview\
29 | serpent\
30 | socket.http\
31 | socket.url\
32 | ssl.https\
33 | (rename dkjson json))"
34 |
35 | :lua_preamble "\z
36 | local anise = require('anise')\
37 | local autils = require('otouto.autils')\
38 | local bindings = require('extern.bindings')\
39 | local utilities = require('otouto.utilities')\
40 | local fennel = require('fennel')\
41 | local fennelview = require('fennelview')\
42 | local serpent = require('serpent')\
43 | local http = require('socket.http')\
44 | local url = require('socket.url')\
45 | local https = require('ssl.https')\
46 | local json = require('dkjson')"
47 |
48 | :action (fn [self bot msg group user]
49 | (if (~= msg.from.id bot.config.admin)
50 | :continue
51 | (let [input (utilities.input msg.text)]
52 | (if (not input)
53 | (do (utilities.send_reply msg "Please enter a string to load.") nil)
54 | (let [mode (if (: msg.text_lower :match (f-str "^{bot.config.cmd_pat}fnl"))
55 | :fennel
56 | (: msg.text_lower :match (f-str "^{bot.config.cmd_pat}return"))
57 | :lua_expr
58 | ; else
59 | :lua)
60 | code
61 | (if (= mode :fennel)
62 | (fennel.compileString
63 | (f-str "{self.fennel_preamble}\n(fn [bot msg group user] {input})"))
64 | ; Lua
65 | (let [input (if (= mode :lua_expr) (f-str "return {input}") input)]
66 | (f-str "{self.lua_preamble}\nreturn function (bot, msg, group, user)\n{input}\nend")))
67 | (output err) (load code)
68 | text (if err
69 | (utilities.html_escape err)
70 | (: self :format_output mode err
71 | (xpcall (output) self.err_msg bot msg group user)))]
72 | (utilities.send_reply msg (f-str "{text}
") :html)
73 | nil)))))
74 |
75 | :format_value (fn [mode val depth]
76 | (if (= mode :fennel)
77 | (fennelview val {:depth depth})
78 | (or (= mode :lua) (= mode :lua_expr))
79 | (serpent.block val {:comment false :depth depth})
80 | ; else
81 | "Unknown format mode."))
82 |
83 | :format_output (fn [self mode ...]
84 | (local len (- (select :# ...) 2))
85 | (local max_length 4000)
86 | (if (= len 0)
87 | "Done!"
88 | ; else
89 | (do
90 | (var depth 5)
91 | (var text nil)
92 | (while (or (not text) (and (> (string.len text) 4000) (> depth 0)))
93 | (set text {})
94 | (for [i 1 len]
95 | (tset text i (utilities.html_escape
96 | (self.format_value mode (select (+ i 2) ...) depth))))
97 | (set text (table.concat text "\n"))
98 | (set depth (- depth 1)))
99 | (if (> (string.len text) 4000)
100 | "Output is too large to print."
101 | text))))
102 | }
103 |
--------------------------------------------------------------------------------
/otouto/plugins/core/paged_lists.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | paged_lists.lua
3 | Support for inline buttons and expiration for paged lists.
4 |
5 | This has become such a convoluted piece of code. Here's a quick rundown.
6 | P:send stores a list object and generates a page (via P:page) and sends it,
7 | schedules it for deletion (P:later), and returns its results.
8 | P:action is for local admins (with can_edit_info perm) to configure page
9 | length, list duration, and whether (most) lists are sent in private.
10 | P:callback_action is to handle keyboard events on paged lists, such as
11 | scrolling (generating a new page with P:page) and deleting the keyboard.
12 |
13 | Copyright 2018 topkecleon
14 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
15 | ]]
16 |
17 | local bindings = require('extern.bindings')
18 | local utilities = require('otouto.utilities')
19 | local anise = require('extern.anise')
20 |
21 | local P = {}
22 |
23 | function P:init(bot)
24 | self.kb = {}
25 | self.kb.default = utilities.keyboard('inline_keyboard'):row()
26 | :button('◀', 'callback_data', self.name .. ' prev')
27 | :button('▶', 'callback_data', self.name .. ' next')
28 | :button('🗑', 'callback_data', self.name .. ' del'):serialize()
29 |
30 | self.kb.private = utilities.keyboard('inline_keyboard'):row()
31 | :button('◀', 'callback_data', self.name .. ' prev')
32 | :button('▶', 'callback_data', self.name .. ' next'):serialize()
33 |
34 | bot.database.paged_lists = bot.database.paged_lists or {}
35 | bot.database.groupdata.plists = bot.database.groupdata.plists or {}
36 | bot.database.userdata.plists = bot.database.userdata.plists or {}
37 | self.db = bot.database.paged_lists
38 |
39 | self.default = bot.config.paged_lists
40 |
41 | -- somewhat consistent message width, kinda gross, really sorry
42 | self.blank = string.rep(utilities.char.braille_space, 39)
43 |
44 | -- P.action will let local admins with can_change_info configure the length
45 | -- of pages, duration of lists, and whether or not lists will be sent
46 | -- publicly or privately.
47 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
48 | :t('listconf', true):t('plists', true).table
49 | self.command = 'listconf '
50 | self.doc = ("Change a setting for paged lists in the group. You must have \z
51 | permission to edit group info to use this command. \n\n\z
52 | Settings (defaults parenthesized): \n\z
53 | • length (%s) - The number of items per page. \n\z
54 | • duration (%s) - The period of time after a list is created \z
55 | that its keyboard should expire. This may be a number of \z
56 | seconds or an interval in the tiem format (see /help tiem). \n\z
57 | • private (%s) - Whether (most) lists should be sent \z
58 | in private rather than to the group. This can be set to \z
59 | true or false. \n\n\z
60 | If no setting is given, this text is returned. If a setting is named \z
61 | without a value, its current value will be returned.")
62 | :format(
63 | self.default.page_length,
64 | utilities.tiem.print(self.default.list_duration),
65 | tostring(self.default.private_lists)
66 | )
67 | end
68 |
69 | -- Send a page, store the list, and schedule it for expiration.
70 | -- title and chat_id are optional. chat_id defaults to msg.chat.id unless
71 | -- private_lists is set.
72 | function P:send(bot, msg, array, title, chat_id)
73 | local plists =
74 | bot.database.groupdata.plists[tostring(chat_id or msg.chat.id)] or {}
75 |
76 | if not chat_id then
77 | -- private_lists can be true, false, or nil.
78 | -- True or false overrides the global default, nil does not.
79 | if
80 | plists.private_lists == true
81 | or plists.private_lists == nil and self.default.private_lists
82 | then
83 | chat_id = msg.from.id
84 | plists = {}
85 | else
86 | chat_id = msg.chat.id
87 | end
88 | end
89 |
90 | local list = {
91 | array = array,
92 | title = title,
93 | chat_id = chat_id,
94 | owner = msg.from,
95 | page = 1,
96 | page_length = plists.page_length or self.default.page_length
97 | }
98 | list.page_count = math.ceil(#list.array / list.page_length)
99 | if list.page_count <= 1 then
100 | list.kb = nil
101 | elseif list.owner.id == list.chat_id then
102 | list.kb = 'private'
103 | else
104 | list.kb = 'default'
105 | end
106 |
107 | local success, result = bindings.sendMessage{
108 | chat_id = list.chat_id,
109 | text = self:page(list),
110 | parse_mode = 'html',
111 | disable_web_page_preview = true,
112 | reply_markup = self.kb[list.kb]
113 | }
114 |
115 | if success then
116 | list.message_id = result.result.message_id
117 | self.db[tostring(list.message_id)] = list
118 | bot:do_later(
119 | self.name,
120 | os.time() + (plists.list_duration or self.default.list_duration),
121 | list.message_id
122 | )
123 | end
124 | return success, result
125 | end
126 |
127 | -- Generate a page.
128 | function P:page(list)
129 | local output = {}
130 | if list.title then
131 | table.insert(
132 | output,
133 | '' .. utilities.html_escape(list.title) .. ''
134 | )
135 | end
136 |
137 | local last = list.page * list.page_length
138 | if #list.array == 0 then
139 | table.insert(output, 'This list is empty!')
140 | else
141 | table.insert(
142 | output,
143 | '• ' .. table.concat(
144 | anise.move(list.array, last - list.page_length + 1, last, 1, {}),
145 | '\n• '
146 | )
147 | )
148 | end
149 |
150 | if list.page_count > 1 then
151 | table.insert(output, self.blank)
152 | table.insert(
153 | output,
154 | string.format(
155 | 'Page %d of %d | %d total
',
156 | list.page,
157 | list.page_count,
158 | #list.array
159 | )
160 | )
161 | end
162 |
163 | return table.concat(output, '\n')
164 | end
165 |
166 | -- For P.action
167 | P.conf = {
168 | length = function(plists, num)
169 | if not num then
170 | return plists.page_length
171 | elseif tonumber(num) then
172 | num = math.floor(math.abs(tonumber(num)))
173 | if num < 1 or num > 40 then
174 | return 'The range for page length is 1-40.'
175 | else
176 | plists.page_length = num
177 | return 'Page length is now ' .. num .. '.'
178 | end
179 | else
180 | return 'Page length must be a number.'
181 | end
182 | end,
183 |
184 | duration = function(plists, dur)
185 | if not dur then
186 | return utilities.tiem.print(plists.list_duration)
187 | elseif utilities.tiem.deformat(dur) then
188 | local interval = utilities.tiem.deformat(dur)
189 | if interval < 60 or interval > 86400 then
190 | return 'The range for list duration is one minute through one day.'
191 | else
192 | plists.list_duration = interval
193 | return 'The list duration is now ' ..
194 | utilities.tiem.print(interval) .. '.'
195 | end
196 | else
197 | return 'The list duration must be an interval (see /help tiem).'
198 | end
199 | end,
200 |
201 | private = function(plists, bool)
202 | if not bool then
203 | bool = tostring(not plists.private_lists)
204 | end
205 |
206 | if bool:lower() == 'true' then
207 | plists.private_lists = true
208 | return 'Most lists will now be sent privately.'
209 | elseif bool:lower() == 'false' then
210 | plists.private_lists = false
211 | return 'Lists will no longer be sent privately.'
212 | else
213 | return 'This setting is true/false.'
214 | end
215 | end
216 | }
217 |
218 | -- For local admins to configure paged lists in the group.
219 | function P:action(bot, msg, group, user)
220 | local _, result = bindings.getChatMember{
221 | chat_id = msg.chat.id,
222 | user_id = msg.from.id
223 | }
224 | if result.result.can_change_info or result.result.status == 'creator' then
225 | local plists
226 | if group and group.data.plists then
227 | plists = group.data.plists
228 | elseif user and user.data.plists then
229 | plists = user.data.plists
230 | else
231 | plists = anise.clone(self.default)
232 | end
233 |
234 | local setting = utilities.get_word(msg.text:lower(), 2)
235 | setting = setting and setting:lower()
236 | if setting and self.conf[setting] then
237 | if group then
238 | group.data.plists = plists
239 | else
240 | user.data.plists = plists
241 | end
242 |
243 | local value = utilities.get_word(msg.text:lower(), 3)
244 | utilities.send_reply(msg, self.conf[setting](plists, value))
245 | else
246 | local output = utilities.plugin_help(bot.config.cmd_pat, self) ..
247 | ("\n\nCurrent settings: \n\z
248 | • length - %s \n\z
249 | • duration - %s \n\z
250 | • private - %s"):format(
251 | plists.page_length,
252 | utilities.tiem.print(plists.list_duration),
253 | tostring(plists.private_lists)
254 | )
255 | utilities.send_reply(msg, output, 'html')
256 | end
257 | else
258 | utilities.send_reply(msg, 'You need permission to edit group info.')
259 | end
260 | end
261 |
262 | -- For the inline keyboard buttons on lists.
263 | function P:callback_action(_, query)
264 | local list = self.db[tostring(query.message.message_id)]
265 | if not list then -- Remove the keyboard from a list we're not storing.
266 | bindings.editMessageReplyMarkup{
267 | chat_id = query.message.chat.id,
268 | message_id = query.message.message_id
269 | }
270 | elseif query.from.id ~= list.owner.id then
271 | bindings.answerCallbackQuery{
272 | callback_query_id = query.id,
273 | text = 'Only ' .. list.owner.first_name .. ' may use this keyboard.'
274 | }
275 | else
276 | local command = utilities.get_word(query.data, 2)
277 | if command == 'del' then
278 | bindings.deleteMessage{
279 | chat_id = list.chat_id,
280 | message_id = list.message_id
281 | }
282 | self.db[tostring(query.message.message_id)] = nil
283 |
284 | elseif command == 'next' then
285 | if list.page == list.page_count then
286 | list.page = 1
287 | else
288 | list.page = list.page + 1
289 | end
290 |
291 | elseif command == 'prev' then
292 | if list.page == 1 then
293 | list.page = list.page_count
294 | else
295 | list.page = list.page - 1
296 | end
297 | end
298 | bindings.editMessageText{
299 | chat_id = list.chat_id,
300 | message_id = list.message_id,
301 | text = self:page(list),
302 | parse_mode = 'html',
303 | disable_web_page_preview = true,
304 | reply_markup = self.kb[list.kb]
305 | }
306 | end
307 | end
308 |
309 | -- For the expiration of lists.
310 | function P:later(_, list_id)
311 | local list = self.db[tostring(list_id)]
312 | if list then
313 | if list.page_count ~= 1 then
314 | bindings.editMessageReplyMarkup{
315 | chat_id = list.chat_id,
316 | message_id = list.message_id
317 | }
318 | end
319 | self.db[tostring(list_id)] = nil
320 | end
321 | end
322 |
323 | return P
324 |
--------------------------------------------------------------------------------
/otouto/plugins/core/ping.fnl:
--------------------------------------------------------------------------------
1 | ;; ping.fnl
2 | ;; Sends a response, then updates it with the time it took to send.
3 |
4 | ;; Copyright 2018 topkecleon
5 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (require-macros :anise.macros)
8 | (require* socket
9 | extern.bindings
10 | otouto.utilities)
11 |
12 | {
13 | :init (fn [self bot]
14 | (set self.command "ping")
15 | (set self.doc "Pong!\z
16 | \nUpdates the message with the time used, in seconds, to send the response.")
17 | (set self.triggers (utilities.make_triggers bot [] :ping :marco :annyong))
18 | (values))
19 |
20 | :action (fn [self bot msg]
21 | (local a (socket.gettime))
22 | (local answer (if (: msg.text_lower :match :marco)
23 | "Polo!"
24 | (: msg.text_lower :match :annyong)
25 | "Annyong."
26 | ; else
27 | "Pong!"))
28 | (local (success message) (utilities.send_reply msg answer))
29 | (local b (string.format "%.3f" (- (socket.gettime) a)))
30 | (if success (bindings.editMessageText {
31 | :chat_id msg.chat.id
32 | :message_id message.result.message_id
33 | :text (f-str "{answer}\n`{b}`")
34 | :parse_mode :Markdown
35 | })))
36 | }
37 |
--------------------------------------------------------------------------------
/otouto/plugins/core/user_blacklist.fnl:
--------------------------------------------------------------------------------
1 | ;; blacklist.lua
2 | ;; Allows the bot owner to block individuals from using the bot.
3 |
4 | ;; Load this before any plugin you want to block blacklisted users from.
5 |
6 | ;; Copyright 2016 topkecleon
7 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
8 |
9 | (require-macros :anise.macros)
10 | (require* otouto.utilities
11 | otouto.autils)
12 |
13 | {
14 | :init (fn [self bot]
15 | (set self.triggers [""])
16 | ;(set self.error false)
17 | (tset? bot.database.userdata :blacklisted {}))
18 |
19 | :blacklist (fn [userdata]
20 | (if userdata.blacklisted
21 | " is already blacklisted."
22 | (do
23 | (set userdata.blacklisted true)
24 | " has been blacklisted.")))
25 |
26 | :unblacklist (fn [userdata]
27 | (if (not userdata.blacklisted)
28 | " is not blacklisted."
29 | (do
30 | (set userdata.blacklisted nil)
31 | " has been unblacklisted.")))
32 |
33 | :action (fn [self bot msg _ user]
34 | (if ; non-owner is blacklisted
35 | (and user.data.blacklisted (~= msg.from.id bot.config.admin))
36 | nil
37 | ; else
38 | (let [act (if (: msg.text :match (f-str "^{bot.config.cmd_pat}blacklist"))
39 | self.blacklist
40 | (: msg.text :match (f-str "^{bot.config.cmd_pat}unblacklist"))
41 | self.unblacklist
42 | ; else
43 | nil)]
44 | (if (not (and act (= msg.from.id bot.config.admin)))
45 | :continue
46 | (let [(targets errors) (autils.targets bot msg)]
47 | (each [target (pairs targets)]
48 | (table.insert errors
49 | (let [user (utilities.user bot target)]
50 | (.. (: user :name) (act user.data)))))
51 | (utilities.send_reply msg (table.concat errors "\n") :html))))))
52 |
53 | :list {
54 | :name :blacklist
55 | :title "Blacklisted Users"
56 | :type :userdata
57 | :key :blacklisted
58 | :sudo true
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/otouto/plugins/core/user_info.fnl:
--------------------------------------------------------------------------------
1 | ;; user_info.fnl
2 | ;; Stores usernames, IDs, and display names for users seen by the bot.
3 | ;; Worthless on its own but prerequisite for some plugins.
4 |
5 | ;; config.user_info.admin_only - set to true to only store user info from
6 | ;; administrated groups.
7 |
8 | ;; Copyright 2018 topkecleon
9 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
10 |
11 | {
12 | :init (fn [self bot]
13 | (set self.triggers [""])
14 | (set self.errors false)
15 | (local users (or bot.database.userdata.info {}))
16 | (set bot.database.userdata.info users)
17 | (tset users (tostring bot.info.id) bot.info)
18 | (set self.administration (or bot.config.user_info.admin_only nil))
19 | (values))
20 |
21 | :action (fn [self bot msg]
22 | (local users bot.database.userdata.info)
23 | (tset users (tostring msg.from.id) msg.from)
24 | (if msg.entities
25 | (each [_ entity (ipairs msg.entities)]
26 | (if entity.user
27 | (tset users (tostring entity.user.id) entity.user))))
28 | (if
29 | msg.reply_to_message
30 | (tset users (tostring msg.reply_to_message.from.id) msg.reply_to_message.from)
31 | msg.forward_from
32 | (tset users (tostring msg.forward_from.id) msg.forward_from)
33 | msg.new_chat_member
34 | (tset users (tostring msg.new_chat_member.id) msg.new_chat_member)
35 | msg.left_chat_member
36 | (tset users (tostring msg.left_chat_member.id) msg.left_chat_member)
37 | ; else
38 | nil)
39 | :continue)
40 |
41 | :list {
42 | :name :users
43 | :title "Known Users"
44 | :type :userdata
45 | :key :info
46 | :sudo true
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/otouto/plugins/core/user_lists.fnl:
--------------------------------------------------------------------------------
1 | ; user_lists.fnl
2 | ; Paged lists of formatted names.
3 |
4 | (local utils (require :otouto.utilities))
5 | (local anise (require :extern.anise))
6 |
7 | (local load_and_unload (fn [self _ plugins]
8 | (set self.lists {})
9 | (set self.cats {:main {} :sudo {} :admin {}})
10 | (each [_ plugin (pairs plugins)]
11 | (when plugin.list
12 | (tset self.lists plugin.list.name plugin.list)
13 | (table.insert (if
14 | plugin.list.sudo self.cats.sudo
15 | (= plugin.list.type :admin) self.cats.admin
16 | self.cats.main)
17 | plugin.list.name)))))
18 |
19 | {
20 | :init (fn [self bot]
21 | (assert (. bot.named_plugins :core.paged_lists)
22 | (.. self.name " requires core.paged_lists"))
23 |
24 | (when bot.config.user_lists.reverse_sort (set self.gt (fn [a b] (> a b))))
25 |
26 | (set self.triggers (utils.make_triggers bot {} [:lists? true]))
27 | (set self.command "list ")
28 | (set self.doc "Returns a paged list of users in the specified category."))
29 |
30 | :action (fn [self bot msg group]
31 | (let [key (utils.get_word msg.text_lower 2) linfo (. self.lists key)]
32 | (if (not linfo)
33 | (utils.send_reply msg (: self :listcats bot msg) :html)
34 | (and (= linfo.type :admin) (or (not group) (not group.data.admin)))
35 | (utils.send_reply msg "This list is available in administrated groups.")
36 | (and linfo.sudo (~= msg.from.id bot.config.admin))
37 | (utils.send_reply msg "You must be the bot owner to see this list.")
38 | ; else
39 | (let [arr (anise.sort (utils.list_names bot (. (if
40 | (= linfo.type :userdata) bot.database.userdata
41 | (= linfo.type :groupdata) bot.database.groupdata
42 | (= linfo.type :admin) group.data.admin)
43 | linfo.key))
44 | self.gt)
45 | (success result) (: (. bot.named_plugins :core.paged_lists)
46 | :send bot msg arr linfo.title)]
47 | (if success
48 | (when (~= result.result.chat.id msg.chat.id)
49 | (utils.send_reply msg "List sent privately."))
50 | ; else
51 | (utils.send_reply msg (..
52 | "Please message me privately first.") :html))))))
54 |
55 | :listcats (fn [self bot msg]
56 | (.. "Available Lists\n• " (table.concat (anise.pushcat {}
57 | self.cats.main
58 | (if (= bot.config.admin msg.from.id) self.cats.sudo)
59 | (if (. bot.database.groupdata.admin (tostring msg.chat.id)) self.cats.admin))
60 | "\n• ")))
61 |
62 | :on_plugins_load load_and_unload
63 | :on_plugins_unload load_and_unload
64 | }
65 |
--------------------------------------------------------------------------------
/otouto/plugins/user/apod.fnl:
--------------------------------------------------------------------------------
1 | ;; apod.fnl
2 | ;; Returns the NASA astronomy picture of the day, along with related text.
3 |
4 | ;; Credit to @HeitorPB.
5 | ;; Copyright 2018 topkecleon
6 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 |
8 | (require-macros :anise.macros)
9 | (require* (rename dkjson json)
10 | otouto.utilities
11 | socket.url
12 | ssl.https)
13 |
14 | {
15 | :init (fn [self bot]
16 | (set self.url (.. "https://api.nasa.gov/planetary/apod?api_key="
17 | (or bot.config.nasa_api_key "DEMO_KEY")))
18 |
19 | (set self.command "apod [YYYY-MM-DD]")
20 | (set self.doc "Returns the latest Astronomy Picture of the Day from \z
21 | NASA.")
22 | (set self.triggers (utilities.make_triggers bot [] [:apod true]))
23 | (values))
24 |
25 | :action (fn [self bot msg]
26 | (local input (utilities.input msg.text))
27 | (local is_date (and input (: input :match "^%d+%-%d+%-%d+$")))
28 | (local url (.. self.url (and-or is_date (.. "&date=" (url.escape input)) "")))
29 |
30 | (local (jstr code) (https.request url))
31 | (if (~= code 200)
32 | (do (utilities.send_reply msg bot.config.errors.connection) nil)
33 | (let [data (json.decode jstr)]
34 | (if data.error
35 | (do (utilities.send_reply msg bot.config.errors.results) nil)
36 | (let [output (f-str "{} ({})\n{}"
37 | (utilities.html_escape data.title)
38 | (utilities.html_escape (or data.hdurl data.url))
39 | (or is_date (os.date "%F"))
40 | (utilities.html_escape data.explanation))]
41 | (utilities.send_message msg.chat.id output false nil :html)
42 | nil)))))
43 | }
44 |
--------------------------------------------------------------------------------
/otouto/plugins/user/bible.fnl:
--------------------------------------------------------------------------------
1 | ;; bible.fnl
2 | ;; Returns Bible verses.
3 |
4 | ;; Copyright 2018 topkecleon
5 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (require-macros :anise.macros)
8 | (require* otouto.utilities
9 | socket.http
10 | socket.url)
11 |
12 | {
13 | :init (fn [self bot]
14 | (if (not bot.config.biblia_api_key)
15 | (io.write "Missing config value: biblia_api_key.\n\z
16 | \tuser.bible will not work without a key from http://bibliaapi.com.\n")
17 | ;else
18 | (do (set self.url "http://api.biblia.com/v1")
19 | (set self.command "bible ")
20 | (set self.doc "Returns a verse from the American Standard Version of the Bible,\z
21 | or a deuterocanonical/apocryphal verse from the King James Version.\z
22 | Results from Biblia.")
23 | (set self.triggers (utilities.make_triggers bot [] [:bible true] [:b true]))))
24 | (values))
25 |
26 | :biblia_content (fn [self key bible passage]
27 | (http.request
28 | (f-str "{self.url}/bible/content/{bible}.txt?key={key}&passage={}"
29 | (url.escape passage))))
30 |
31 | :action (fn [self bot msg]
32 | (local input (utilities.input_from_msg msg))
33 | (if (not input)
34 | (do (utilities.send_plugin_help msg.chat.id msg.message_id bot.config.cmd_pat self) nil)
35 | (let [key bot.config.biblia_api_key]
36 | (var (output res) (: self :biblia_content key "ASV" input))
37 | (when (or (not output) (~= res 200) (= (: output :len) 0))
38 | (set (output res) (: self :biblia_content key "KJVAPOC" input)))
39 | (when (or (not output) (~= res 200) (= (: output :len) 0))
40 | (set output bot.config.errors.results))
41 | (when (> (: output :len) 4000)
42 | (set output "The text is too long to post here. Try being more specific."))
43 | (utilities.send_reply msg output)
44 | nil)))
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/otouto/plugins/user/calc.fnl:
--------------------------------------------------------------------------------
1 | ;; calc.fnl
2 | ;; Runs mathematical expressions through the mathjs.org API.
3 |
4 | ;; Copyright 2018 topkecleon
5 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (require-macros :anise.macros)
8 | (require* otouto.utilities
9 | socket.url
10 | ssl.https)
11 |
12 | {
13 | :init (fn [self bot]
14 | (set self.url "https://api.mathjs.org/v4/?expr=")
15 |
16 | (set self.command "calc ")
17 | (set self.doc "Returns solutions to mathematical expressions and \z
18 | conversions between common units. Results provided by mathjs.org.")
19 | (set self.triggers (utilities.make_triggers bot [] [:calc true]))
20 | (values))
21 |
22 | :action (fn [self bot msg]
23 | (local input (utilities.input_from_msg msg))
24 | (if (not input)
25 | (do (utilities.send_plugin_help msg.chat.id msg.message_id bot.config.cmd_pat self) nil)
26 | (let [(data res) (.. (https.request (.. self.url (url.escape input))))
27 | output (and-or data
28 | (f-str "{}
" (utilities.html_escape data))
29 | bot.config.errors.connection)]
30 | (utilities.send_reply msg output :html)
31 | nil)))
32 | }
33 |
--------------------------------------------------------------------------------
/otouto/plugins/user/cat_fact.fnl:
--------------------------------------------------------------------------------
1 | ;; cat_fact.fnl
2 | ;; Returns cat facts.
3 |
4 | ;; Based on a plugin by matthewhesketh.
5 | ;; Copyright 2018 topkecleon
6 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 |
8 | (require-macros :anise.macros)
9 | (require* (rename dkjson json)
10 | otouto.utilities
11 | ssl.https)
12 |
13 | {
14 | :init (fn [self bot]
15 | (set self.url "https://catfact.ninja/fact")
16 |
17 | (set self.command "catfact")
18 | (set self.doc "Returns a cat fact from catfact.ninja.")
19 | (set self.triggers (utilities.make_triggers bot [] ["cat_?fact"]))
20 | (values))
21 |
22 | :action (fn [self bot msg]
23 | (local (jstr code) (https.request self.url))
24 | (if (~= code 200)
25 | (do (utilities.send_reply msg bot.config.errors.connection) nil)
26 | (let [data (json.decode jstr)
27 | output (f-str "Cat Fact\n{}" (utilities.html_escape data.fact))]
28 | (utilities.send_message msg.chat.id output true nil :html)
29 | nil)))
30 | }
31 |
--------------------------------------------------------------------------------
/otouto/plugins/user/cats.fnl:
--------------------------------------------------------------------------------
1 | ;; cats.fnl
2 | ;; Returns photos of cats from thecatapi.com.
3 |
4 | ;; Copyright 2016 topkecleon
5 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (require-macros :anise.macros)
8 | (require* socket.http
9 | extern.bindings
10 | otouto.utilities)
11 |
12 | { :init
13 | (fn [self bot]
14 | (when (not bot.config.cat_api_key)
15 | (io.write "Missing config value: cat_api_key.\n\z
16 | \tuser.cats will be enabled, but there are more features with a key.\n"))
17 | (set self.url
18 | (.. "http://thecatapi.com/api/images/get?format=html&type=jpg"
19 | (and-or bot.config.cat_api_key
20 | (.. "&api_key=" bot.config.cat_api_key)
21 | "")))
22 |
23 | (set self.command "cat")
24 | (set self.doc "Returns a cat!")
25 | (set self.triggers (utilities.make_triggers bot [] :cat))
26 | (values))
27 |
28 | :action
29 | (fn [self bot msg]
30 | (local (str res) (http.request self.url))
31 | (if (~= res 200)
32 | (do (utilities.send_reply msg bot.config.errors.connection) nil)
33 | (do (bindings.sendPhoto {:chat_id msg.chat.id
34 | :photo (: str :match "
")})
35 | nil)))}
36 |
37 |
--------------------------------------------------------------------------------
/otouto/plugins/user/commit.fnl:
--------------------------------------------------------------------------------
1 | ;; commit.fnl
2 | ;; Returns a commit message from whatthecommit.com.
3 |
4 | ;; Copyright 2019 topkecleon
5 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (local http (require :socket.http))
8 | (local utilities (require :otouto.utilities))
9 | (local bindings (require :extern.bindings))
10 |
11 | {
12 | :init (fn [self bot]
13 | (set self.triggers (utilities.make_triggers bot [] :commit))
14 | (set self.command :commit)
15 | (set self.doc "Returns a commit message from whatthecommit.com.")
16 | (set self.url "http://whatthecommit.com/index.txt")
17 | (set self.alt "add more cowbell")
18 | )
19 |
20 | :action (fn [self _ msg]
21 | (bindings.sendMessage {
22 | :chat_id msg.chat.id
23 | :text (.. "" (or (http.request self.url) self.alt) "
")
24 | :parse_mode :html
25 | })
26 | )
27 | }
--------------------------------------------------------------------------------
/otouto/plugins/user/currency.lua:
--------------------------------------------------------------------------------
1 | local https = require('ssl.https')
2 | local json = require('dkjson')
3 |
4 | local utilities = require('otouto.utilities')
5 | local anise = require('extern.anise')
6 |
7 | local p = {}
8 |
9 | function p:init(bot)
10 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
11 | :t('cash', true):t('currency', true).table
12 | self.command = 'cash [amount] to '
13 | self.doc = 'Example: ' .. bot.config.cmd_pat .. [[cash 5 BTC to USD
14 | Returns currency exchange rates and calculations.
15 | Source: Exchange Rates API
16 | Alias: ]] .. bot.config.cmd_pat .. 'currency'
17 | self.url = "https://api.exchangeratesapi.io/latest"
18 | self.out_str = '%s %s = %s %s\nRate: %s\n%s\nExchange Rates API'
19 | self.unsupported_str = 'Unsupported currency: '
20 | bot.database[self.name] = bot.database[self.name] or {}
21 | self.db = bot.database[self.name]
22 | self:later(bot)
23 | end
24 |
25 | function p:get_rate(from, to)
26 | if from == self.db.base then
27 | return self.db.rates[to]
28 | elseif to == self.db.base then
29 | return 1 / self.db.rates[from]
30 | else
31 | return (1 / self.db.rates[from]) * self.db.rates[to]
32 | end
33 | end
34 |
35 | function p:action(bot, msg)
36 | if not next(self.db) then
37 | self:update(bot)
38 | if not next(self.db) then
39 | error('No conversion rates available.')
40 | end
41 | end
42 |
43 | local output
44 | local input = msg.text:upper()
45 | local from_cur, to_cur = input:match('(%a%a%a) TO (%a%a%a)')
46 | if from_cur and to_cur then
47 | local from_val = tonumber(
48 | input:match('([%d%.]+) ' .. from_cur .. ' TO ' .. to_cur)
49 | ) or 1
50 | if not (self.db.rates[from_cur] or self.db.base == from_cur) then
51 | output = self.unsupported_str .. from_cur
52 | elseif not (self.db.rates[to_cur] or self.db.base == to_cur) then
53 | output = self.unsupported_str .. to_cur
54 | else
55 | local rate = self:get_rate(from_cur, to_cur)
56 | local to_val = from_val * rate
57 | output = string.format(self.out_str,
58 | from_val,
59 | from_cur,
60 | string.format("%.2f", from_val * rate),
61 | to_cur,
62 | string.format("%.4f", rate),
63 | self.db.date
64 | )
65 | end
66 | else
67 | output = self.doc ..
68 | '\n\nThe following currencies can be converted:\n' ..
69 | table.concat(anise.keys(self.db.rates), ' ') .. '
'
70 | end
71 | utilities.send_reply(msg, output, 'html')
72 | end
73 |
74 | -- Update the rates every hour. The API updates every hour.
75 | function p:later(bot)
76 | self:update(bot)
77 | bot:do_later(self.name, os.time() + (60 * 60))
78 | end
79 |
80 | function p:update(bot)
81 | local jstr, code = https.request(self.url)
82 | if code == 200 then
83 | bot.database[self.name] = json.decode(jstr)
84 | self.db = bot.database[self.name]
85 | end
86 | end
87 |
88 | return p
89 |
--------------------------------------------------------------------------------
/otouto/plugins/user/data/eight_ball.fnl:
--------------------------------------------------------------------------------
1 | { :yesno
2 | ['Yes.'
3 | 'No.'
4 | 'Absolutely.'
5 | 'In your dreams.']
6 |
7 | :answers
8 | ["It is certain."
9 | "It is decidedly so."
10 | "Without a doubt."
11 | "Yes, definitely."
12 | "You may rely on it."
13 | "As I see it, yes."
14 | "Most likely."
15 | "Outlook: good."
16 | "Yes."
17 | "Signs point to yes."
18 | "Reply hazy try again."
19 | "Ask again later."
20 | "Better not tell you now."
21 | "Cannot predict now."
22 | "Concentrate and ask again."
23 | "Don't count on it."
24 | "My reply is no."
25 | "My sources say no."
26 | "Outlook: not so good."
27 | "Very doubtful."]}
28 |
29 |
--------------------------------------------------------------------------------
/otouto/plugins/user/data/slap.fnl:
--------------------------------------------------------------------------------
1 | [
2 | "VICTIM was shot by VICTOR."
3 | "VICTIM was pricked to death."
4 | "VICTIM walked into a cactus while trying to escape VICTOR."
5 | "VICTIM drowned."
6 | "VICTIM drowned whilst trying to escape VICTOR."
7 | "VICTIM blew up."
8 | "VICTIM was blown up by VICTOR."
9 | "VICTIM hit the ground too hard."
10 | "VICTIM fell from a high place."
11 | "VICTIM fell off a ladder."
12 | "VICTIM fell into a patch of cacti."
13 | "VICTIM was doomed to fall by VICTOR."
14 | "VICTIM was blown from a high place by VICTOR."
15 | "VICTIM was squashed by a falling anvil."
16 | "VICTIM went up in flames."
17 | "VICTIM burned to death."
18 | "VICTIM was burnt to a crisp whilst fighting VICTOR."
19 | "VICTIM walked into a fire whilst fighting VICTOR."
20 | "VICTIM tried to swim in lava."
21 | "VICTIM tried to swim in lava while trying to escape VICTOR."
22 | "VICTIM was struck by lightning."
23 | "VICTIM was slain by VICTOR."
24 | "VICTIM got finished off by VICTOR."
25 | "VICTIM was killed by magic."
26 | "VICTIM was killed by VICTOR using magic."
27 | "VICTIM starved to death."
28 | "VICTIM suffocated in a wall."
29 | "VICTIM fell out of the world."
30 | "VICTIM was knocked into the void by VICTOR."
31 | "VICTIM withered away."
32 | "VICTIM was pummeled by VICTOR."
33 | "VICTIM was fragged by VICTOR."
34 | "VICTIM was desynchronized."
35 | "VICTIM was wasted."
36 | "VICTIM was busted."
37 | "VICTIM's bones are scraped clean by the desolate wind."
38 | "VICTIM has died of dysentery."
39 | "VICTIM fainted."
40 | "VICTIM is out of usable Pokemon! VICTIM whited out!"
41 | "VICTIM is out of usable Pokemon! VICTIM blacked out!"
42 | "VICTIM whited out!"
43 | "VICTIM blacked out!"
44 | "VICTIM says goodbye to this cruel world."
45 | "VICTIM got rekt."
46 | "VICTIM was sawn in half by VICTOR."
47 | "VICTIM died. I blame VICTOR."
48 | "VICTIM was axe-murdered by VICTOR."
49 | "VICTIM's melon was split by VICTOR."
50 | "VICTIM was sliced and diced by VICTOR."
51 | "VICTIM was split from crotch to sternum by VICTOR."
52 | "VICTIM's death put another notch in VICTOR's axe."
53 | "VICTIM died impossibly!"
54 | "VICTIM died from VICTOR's mysterious tropical disease."
55 | "VICTIM escaped infection by dying."
56 | "VICTIM played hot-potato with a grenade."
57 | "VICTIM was knifed by VICTOR."
58 | "VICTIM fell on his sword."
59 | "VICTIM ate a grenade."
60 | "VICTIM practiced being VICTOR's clay pigeon."
61 | "VICTIM is what's for dinner!"
62 | "VICTIM was terminated by VICTOR."
63 | "VICTIM was shot before being thrown out of a plane."
64 | "VICTIM was not invincible."
65 | "VICTIM has encountered an error."
66 | "VICTIM died and reincarnated as a goat."
67 | "VICTOR threw VICTIM off a building."
68 | "VICTIM is sleeping with the fishes."
69 | "VICTIM got a premature burial."
70 | "VICTOR replaced all of VICTIM's music with Nickelback."
71 | "VICTOR spammed VICTIM's email."
72 | "VICTOR made VICTIM a knuckle sandwich."
73 | "VICTOR slapped VICTIM with pure nothing."
74 | "VICTOR hit VICTIM with a small, interstellar spaceship."
75 | "VICTIM was quickscoped by VICTOR."
76 | "VICTOR put VICTIM in check-mate."
77 | "VICTOR RSA-encrypted VICTIM and deleted the private key."
78 | "VICTOR put VICTIM in the friendzone."
79 | "VICTOR slaps VICTIM with a DMCA takedown request!"
80 | "VICTIM became a corpse blanket for VICTOR."
81 | "Death is when the monsters get you. Death comes for VICTIM."
82 | "Cowards die many times before their death. VICTIM never tasted death but once."
83 | "VICTIM died of hospital gangrene."
84 | "VICTIM got a house call from Doctor VICTOR."
85 | "VICTOR beheaded VICTIM."
86 | "VICTIM got stoned...by an angry mob."
87 | "VICTOR sued the pants off VICTIM."
88 | "VICTIM was impeached."
89 | "VICTIM was one-hit KO'd by VICTOR."
90 | "VICTOR sent VICTIM to /dev/null."
91 | "VICTOR sent VICTIM down the memory hole."
92 | "VICTIM was a mistake."
93 | "\"VICTIM was a mistake.\" - VICTOR"
94 | "VICTOR checkmated VICTIM in two moves."
95 | "VICTIM was made redundant."
96 | "VICTIM was assimilated."
97 | "VICTIM is with Harambe now."
98 | ]
99 |
--------------------------------------------------------------------------------
/otouto/plugins/user/dice.fnl:
--------------------------------------------------------------------------------
1 | ;; dice.fnl
2 | ;; Returns a set of random numbers. Accepts D&D notation.
3 |
4 | ;; Copyright 2018 topkecleon
5 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (require-macros :anise.macros)
8 | (require* otouto.utilities)
9 |
10 | {
11 | :init (fn [self bot]
12 | (set self.command "roll ")
13 | (set self.doc
14 | "Returns a set of dice rolls, where n is the number of rolls and r is the \z
15 | range. If only a range is given, returns only one roll.")
16 | (set self.triggers (utilities.make_triggers bot [] [:roll true]))
17 | (values))
18 |
19 | :action (fn [self bot msg]
20 | (local input (utilities.input msg.text_lower))
21 | (if (not input)
22 | (do (utilities.send_plugin_help msg.chat.id msg.message_id bot.config.cmd_pat self) nil)
23 | (do
24 | (var (count range) (: input :match "([%d]+)d([%d]+)"))
25 | (when (not count)
26 | (set count 1)
27 | (set range (: input :match "d?([%d]+)$")))
28 | (if (not range)
29 | (do (utilities.send_message msg.chat.id self.doc true msg.message_id :html) nil)
30 | (let [count (tonumber count)
31 | range (tonumber range)]
32 | (if (< range 2)
33 | (do (utilities.send_reply msg "The minimum range is 2.") nil)
34 | (or (> range 1000) (> count 1000))
35 | (do (utilities.send_reply msg "The maximum range and count are 1000.") nil)
36 | ; else
37 | (let [output (f-str "{count}d{range}\n")]
38 | (var output output)
39 | (for [_ 1 count]
40 | (set output (f-str "{output}{}\t" (math.random range))))
41 | (set output (f-str "{output}
"))
42 | (utilities.send_message msg.chat.id output true msg.message_id :html)
43 | nil)))))))
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/otouto/plugins/user/dilbert.fnl:
--------------------------------------------------------------------------------
1 | ; dilbert.fnl
2 | ; Copyright 2018 topkecleon
3 | ; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
4 |
5 | (require-macros :anise.macros)
6 | (require* ssl.https
7 | socket.url
8 | extern.bindings
9 | otouto.utilities)
10 |
11 | {
12 | :init (fn [self bot]
13 | (set self.command "dilbert [date]")
14 | (set self.doc (.. bot.config.cmd_pat "dilbert [YYYY-MM-DD] \
15 | Returns the latest Dilbert strip or that of a given date."))
16 | (set self.triggers (utilities.make_triggers bot [] [:dilbert true])))
17 |
18 | :action (fn [self bot msg] (let [input (utilities.get_word msg.text 2)]
19 | (local (res code)
20 | (https.request (.. "https://dilbert.com/strip/" (url.escape (or
21 | (and input (: input :match "^%d%d%d%d%-%d%d%-%d%d$"))
22 | (os.date "%F"))))))
23 | (if (~= code 200)
24 | (do (utilities.send_reply msg bot.config.errors.connection) nil)
25 | ;else
26 | (bindings.sendPhoto {
27 | :chat_id msg.chat.id
28 | :photo (: res :match "")
29 | :caption (: res :match "")})
30 | nil)))
31 | }
32 |
--------------------------------------------------------------------------------
/otouto/plugins/user/dump.lua:
--------------------------------------------------------------------------------
1 | local utilities = require('otouto.utilities')
2 | local bindings = require('extern.bindings')
3 | local json = require('dkjson')
4 |
5 | local P = {}
6 |
7 | function P:init(bot)
8 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
9 | :t('dump').table
10 | self.command = 'dump'
11 | self.doc = 'Dumps JSON of the replied-to message or the command.'
12 | end
13 |
14 | function P:action(bot, msg)
15 | if msg.reply_to_message then
16 | bindings.sendMessage{
17 | chat_id = msg.chat.id,
18 | reply_to_message_id = msg.message_id,
19 | text = '' .. utilities.html_escape(
20 | json.encode(msg.reply_to_message, { indent = true })
21 | ) .. '
',
22 | parse_mode = 'HTML'
23 | }
24 | else
25 | bindings.sendMessage{
26 | chat_id = msg.chat.id,
27 | reply_to_message_id = msg.message_id,
28 | text = '' .. utilities.html_escape(
29 | json.encode(msg, { indent = true })
30 | ) .. '
',
31 | parse_mode = 'HTML'
32 | }
33 | end
34 | end
35 |
36 | return P
37 |
--------------------------------------------------------------------------------
/otouto/plugins/user/echo.fnl:
--------------------------------------------------------------------------------
1 | ;; echo.fnl
2 | ;; Returns input.
3 |
4 | ;; Copyright 2018 topkecleon
5 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (require-macros :anise.macros)
8 | (require* serpent
9 | otouto.utilities)
10 |
11 | {
12 | :init (fn [self bot]
13 | (set self.command "echo ")
14 | (set self.doc "Repeats a string of text.")
15 | (set self.triggers (utilities.make_triggers bot [] [:echo true]))
16 | (values))
17 |
18 | :action (fn [self bot msg]
19 | (local input (utilities.input_from_msg msg))
20 | (if (not input)
21 | (do (utilities.send_plugin_help msg.chat.id msg.message_id bot.config.cmd_pat self) nil)
22 | (let [html_input (utilities.html_escape input)
23 | output (if (= msg.chat.type :supergroup)
24 | (f-str "Echo:\n\"{}\"" (utilities.html_escape input))
25 | (utilities.html_escape (.. utilities.char.zwnj input)))]
26 | (utilities.send_message msg.chat.id output true nil :html)
27 | nil)))
28 | }
29 |
--------------------------------------------------------------------------------
/otouto/plugins/user/eight_ball.fnl:
--------------------------------------------------------------------------------
1 | ;; eight_ball.fnl
2 | ;; Returns magic 8-ball like answers.
3 |
4 | ;; Copyright 2018 topkecleon
5 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (require-macros :anise.macros)
8 | (require* otouto.utilities
9 | (rename otouto.plugins.user.data.eight_ball data))
10 |
11 | { :init
12 | (fn [self bot]
13 | (set self.command "8ball")
14 | (set self.doc "Returns an answer from a magic 8-ball!")
15 | (set self.triggers (utilities.make_triggers bot ["[Yy]/[Nn]%p*$"] [:8ball true]))
16 | (values))
17 |
18 | :action
19 | (fn [self _ msg]
20 | (utilities.send_reply
21 | msg
22 | (and-or (: msg.text_lower :match "y/n%p?$")
23 | (. data.yesno (math.random (# data.yesno)))
24 | (. data.answers (math.random (# data.answers))))))}
25 |
26 |
--------------------------------------------------------------------------------
/otouto/plugins/user/full_width.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | full_width.lua
3 | otouto plugin to convert Latin text to Latin full-width text.
4 |
5 | Copyright 2017 topkecleon
6 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 | ]]
8 |
9 | local utilities = require('otouto.utilities')
10 |
11 | local P = {}
12 |
13 | function P:init(bot)
14 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
15 | :t('full_?width', true).table
16 | self.command = 'fullwidth '
17 | self.doc = 'Returns aesthetic text.'
18 | end
19 |
20 | function P:action(bot, msg)
21 | local input = utilities.input_from_msg(msg)
22 | if not input then
23 | utilities.send_reply(msg, self.doc, 'html')
24 | return
25 | end
26 | -- Piece together the output by iterating through every character and
27 | -- changing Latin characters to their full-width counterparts. Characters
28 | -- which are not in the "Basic Latin" set will be ignored.
29 | -- Error handling to check whether or not a given
30 | -- character is in the basic latin set.
31 | local output = {}
32 |
33 | for char in input:gmatch('.') do
34 | local succ, code = pcall(function() return utf8.codepoint(char) end)
35 | -- Full-width codepoints are 65248 higher than their basic counterparts.
36 | if succ and code >= 33 and code <= 126 then
37 | table.insert(output, utf8.char(code+65248))
38 | else
39 | table.insert(output, char)
40 | end
41 | end
42 |
43 | utilities.send_reply(msg, table.concat(output))
44 | end
45 |
46 | return P
47 |
48 |
--------------------------------------------------------------------------------
/otouto/plugins/user/google_translate.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | google_translate.lua
3 | A plugin for the Google translate API.
4 |
5 | Uses config.lang for the output language, unless specified.
6 |
7 | Copyright 2017 topkecleon
8 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
9 | ]]--
10 |
11 | local https = require('ssl.https')
12 | local url = require('socket.url')
13 | local dkjson = require('dkjson')
14 |
15 | local utilities = require('otouto.utilities')
16 |
17 | local tl = {}
18 |
19 | function tl:init(bot)
20 | if not bot.config.google_api_key then
21 | io.write("Missing config value: google_api_key.\n\z
22 | \tuser.google_translate will not work without a Google API key.\n")
23 | else
24 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
25 | :t('g?translate', true):t('g?tl', true).table
26 | self.command = 'translate '
27 | self.doc = bot.config.cmd_pat .. [[translate [lang] (in reply)
28 | Translates input or the replied-to message into the bot's default language.
29 | In non-reply commands, $text follows a line break after the command and language code.
30 | Translation service provided by Google.
31 | Aliases: /gtranslate, /tl, /gtl.]]
32 | -- "gtl" = "good translate"
33 | self.url = 'https://translation.googleapis.com/language/translate/v2?key=' ..
34 | bot.config.google_api_key .. '&format=text&target=%s&q=%s'
35 | end
36 | end
37 |
38 | function tl:action(bot, msg)
39 | local lang = bot.config.lang
40 | local text = utilities.input(msg.text)
41 |
42 | if msg.reply_to_message and #msg.reply_to_message.text > 0 then
43 | if text and text:len() == 2 then
44 | lang = text:lower()
45 | end
46 | text = msg.reply_to_message.text
47 |
48 | else
49 | if text and text:match('^..\n.') then
50 | lang, text = text:match('^(..)\n(.+)$')
51 | end
52 | end
53 |
54 | if not text then
55 | utilities.send_reply(msg, self.doc, 'html')
56 | return
57 | end
58 |
59 | local result, code = https.request(self.url:format(lang, url.escape(text)))
60 | if code ~= 200 then
61 | utilities.send_reply(msg, bot.config.errors.connection)
62 | return
63 | end
64 |
65 | local data = dkjson.decode(result)
66 | if not data.data.translations[1] then
67 | utilities.send_reply(msg, bot.config.errors.results)
68 | return
69 | end
70 |
71 | local output = string.format('%s → %s:\n%s',
72 | data.data.translations[1].detectedSourceLanguage:upper(),
73 | lang:upper(),
74 | utilities.html_escape(data.data.translations[1].translatedText)
75 | )
76 | utilities.send_reply(msg.reply_to_message or msg, output, 'html')
77 | end
78 |
79 | return tl
80 |
81 |
--------------------------------------------------------------------------------
/otouto/plugins/user/greetings.fnl:
--------------------------------------------------------------------------------
1 | ; greetings.fnl
2 | ; Reponds to configurable terms with configurable greetings.
3 |
4 | ; Copyright 2019 topkecleon
5 | ; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (require-macros :anise.macros)
8 | (require* otouto.utilities)
9 |
10 | {
11 | :init (fn [self bot]
12 | (set self.triggers {})
13 | (each [_ triggers (pairs bot.config.greetings)]
14 | (each [_ trigger (ipairs triggers)]
15 | (let [s (f-str "^{trigger},? {}%p*$" (: bot.info.first_name :lower))]
16 | (table.insert self.triggers s)))))
17 |
18 | :action (fn [_ bot msg]
19 | (var output "")
20 | (each [response triggers (pairs bot.config.greetings)]
21 | (each [_ trigger (ipairs triggers)]
22 | (when (: msg.text_lower :match trigger)
23 | (set output response))))
24 | (let [nick (: (utilities.get_nick bot msg.from) :gsub "%%" "%%%%")]
25 | (set output (: output :gsub "#NAME" nick)))
26 | (utilities.send_reply msg output))
27 | }
--------------------------------------------------------------------------------
/otouto/plugins/user/hex_color.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | hex_color.lua
3 | Returns an image of the given color code in hexadecimal format.
4 |
5 | If colorhexa.com ever stops working for any reason, it would be simple to
6 | generate these images on-the-fly with ImageMagick installed, like so:
7 | os.execute(string.format(
8 | 'convert -size 128x128 xc:#%s /tmp/%s.png',
9 | hex,
10 | hex
11 | ))
12 | Or alternatively, use a magic table to produce and store them.
13 | local colors = {}
14 | setmetatable(colors, { __index = function(tab, key)
15 | filename = '/tmp/' .. key .. '.png'
16 | os.execute('convert -size 128x128 xc:#' .. key .. ' ' .. filename)
17 | tab[key] = filename
18 | return filename
19 | end})
20 |
21 | Copyright 2016 topkecleon
22 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
23 | ]]--
24 |
25 | local bindings = require('extern.bindings')
26 | local utilities = require('otouto.utilities')
27 |
28 | local P = {}
29 |
30 | function P:init(bot)
31 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
32 | :t('colou?r', true):t('hex', true).table
33 | self.command = 'color [ffffff]'
34 | self.doc = [[Returns an image of the given color code. Color codes must be in hexadecimal.
35 | Acceptable code formats:
36 | ABCDEF -> #ABCDEF
37 | F96 -> #FF9966
38 | F5 -> #F5F5F5
39 | F -> #FFFFFF
40 | The preceding hash symbol is optional.]]
41 | self.url = 'http://www.colorhexa.com/%s.png'
42 | self.invalid_number_error = 'Invalid number. See ' .. bot.config.cmd_pat .. 'help color'
43 | end
44 |
45 | function P:action(bot, msg)
46 | local input = utilities.get_word(msg.text, 2)
47 | if input then
48 | input = input:gsub('#', '')
49 | input_is_number = tonumber('0x' .. input)
50 | if input_is_number and (#input <= 3 or #input == 6) then
51 | local hex
52 | if #input == 1 then
53 | hex = input:rep(6)
54 | elseif #input == 2 then
55 | hex = input:rep(3)
56 | elseif #input == 3 then
57 | hex = ''
58 | for s in input:gmatch('.') do
59 | hex = hex .. s .. s
60 | end
61 | elseif #input == 6 then
62 | hex = input
63 | end
64 | bindings.sendPhoto{
65 | chat_id = msg.chat.id,
66 | reply_to_message_id = msg.message_id,
67 | photo = self.url:format(hex),
68 | caption = '#' .. hex
69 | }
70 | else
71 | utilities.send_reply(msg, self.invalid_number_error)
72 | end
73 | else
74 | utilities.send_reply(msg, self.doc, 'html')
75 | end
76 | end
77 |
78 | return P
79 |
--------------------------------------------------------------------------------
/otouto/plugins/user/lastfm.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | lastfm.lua
3 | Returns "now playing" info from last.fm.
4 |
5 | Copyright 2019 topkecleon
6 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 | ]]--
8 |
9 | local url = require('socket.url')
10 | local http = require('socket.http')
11 | local json = require('dkjson')
12 |
13 | local utilities = require('otouto.utilities')
14 | local bindings = require('extern.bindings')
15 |
16 | local p = {}
17 |
18 | function p:init(bot)
19 | assert(bot.config.lastfm_api_key,
20 | 'Missing config value: lastfm_api_key. user.lastfm will not work \z
21 | without a last.fm API key.')
22 | self.url = 'http://ws.audioscrobbler.com/2.0/?method=user.getRecentTracks&api_key=' ..
23 | bot.config.lastfm_api_key .. '&format=json&limit=1&user='
24 | self.test_url = 'http://ws.audioscrobbler.com/2.0/?method=user.getInfo&api_key=' ..
25 | bot.config.lastfm_api_key .. '&format=json&user='
26 |
27 | self.command = 'np [username]'
28 | self.doc = "Shows the currently playing or last played track \z
29 | from last.fm.\nYou may specify your last.fm username after the \z
30 | command. After that, it will be stored. Otherwise, your Telegram \z
31 | username will be tried. You may delete your stored last.fm username \z
32 | with " .. bot.config.cmd_pat .. "np --."
33 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
34 | :t('np', true).table
35 |
36 | if not bot.database.userdata.lastfm then
37 | bot.database.userdata.lastfm = {}
38 | end
39 |
40 | self.notices = {
41 | deleted = 'Your last.fm username has been deleted.',
42 | invalid = 'Invalid last.fm username.',
43 | specify = 'Please specify your last.fm username, eg ' ..
44 | bot.config.cmd_pat .. 'np durov'
45 | }
46 | end
47 |
48 | function p:test_username(uname)
49 | local jstr, res = http.request(self.test_url .. url.escape(uname))
50 | if res == 200 then
51 | if json.decode(jstr).error then
52 | return false
53 | else
54 | return true
55 | end
56 | else
57 | return false
58 | end
59 | end
60 |
61 | function p.format_track(track, dname)
62 | return string.format(
63 | 'Now Playing for %s\n🎵 %s\n👤 %s\n💿 %s',
64 | utilities.html_escape(dname),
65 | track.url,
66 | utilities.html_escape(track.name),
67 | utilities.html_escape(track.artist["#text"]),
68 | utilities.html_escape(track.album["#text"])
69 | )
70 | end
71 |
72 | function p.format_error(data)
73 | return string.format(
74 | 'Error %s: %s',
75 | data.error,
76 | data.message
77 | )
78 | end
79 |
80 | -- returns text and optional image URL
81 | -- Whether or not the URL is returned determines whether sendMessage is used
82 | -- or sendPhoto is used.
83 | function p:fetch(uname, dname)
84 | local jstr, res = http.request(self.url .. url.escape(uname))
85 | local output, img_url
86 | if res == 200 then
87 | local data = json.decode(jstr)
88 | if data.recenttracks then
89 | local track = data.recenttracks.track[1]
90 | for _, image in pairs(track.image) do
91 | if image.size == 'extralarge' then
92 | img_url = image["#text"]
93 | break
94 | end
95 | end
96 | output = self.format_track(track, dname)
97 | elseif data.error then
98 | output = self.format_error(data)
99 | else
100 | error("Unknown response from last.fm API.\n" .. jstr)
101 | end
102 | else
103 | output = false
104 | end
105 | return output, img_url
106 | end
107 |
108 | function p:action(bot, msg, _, user)
109 | local input = utilities.input(msg.text)
110 | local dname = user:display_name()
111 | -- Returned by self.fetch. If img_url is present, sendPhoto is used.
112 | local output, img_url
113 |
114 | -- Delete username.
115 | if input == '--' or input == utilities.char.em_dash then
116 | user.data.lastfm = nil
117 | output = self.notices.deleted
118 |
119 | -- Set username to input, or to TG username if user.data.lastfm not set.
120 | elseif input or (msg.from.username and not user.data.lastfm) then
121 | -- err if invalid username
122 | local new_username = input or msg.from.username
123 | local valid_username = self:test_username(new_username)
124 | if valid_username then
125 | user.data.lastfm = new_username
126 | output, img_url = self:fetch(new_username, dname)
127 | output = output .. "\n\nYour last.fm username has been set to " ..
128 | utilities.html_escape(new_username) .. ". You may change \z
129 | it by specifying your last.fm username in the command, eg " ..
130 | bot.config.cmd_pat .. "np durov"
131 | elseif input then
132 | output = self.notices.invalid
133 | else
134 | output = self.notices.specify
135 | end
136 |
137 | -- Use user.data.lastfm.
138 | elseif user.data.lastfm then -- results
139 | output, img_url = self:fetch(user.data.lastfm, dname)
140 |
141 | else
142 | output = self.notices.specify
143 | end
144 |
145 | if img_url then
146 | bindings.sendPhoto{
147 | photo = img_url,
148 | caption = output,
149 | parse_mode = 'html',
150 | chat_id = msg.chat.id
151 | }
152 | elseif output then
153 | bindings.sendMessage{
154 | text = output,
155 | parse_mode = 'html',
156 | chat_id = msg.chat.id,
157 | disable_web_page_preview = true
158 | }
159 | else
160 | bindings.sendMessage{
161 | text = bot.config.errors.connection,
162 | chat_id = msg.chat.id,
163 | reply_to_message_id = msg.reply_to_message.message_id
164 | }
165 | end
166 | end
167 |
168 | return p
--------------------------------------------------------------------------------
/otouto/plugins/user/maybe.fnl:
--------------------------------------------------------------------------------
1 | ;; maybe.fnl
2 | ;; Runs a command, if it feels like it.
3 |
4 | ;; Copyright 2016 topkecleon
5 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (require-macros :anise.macros)
8 | (require* anise
9 | otouto.utilities)
10 |
11 | {
12 | :init (fn [self bot]
13 | (set self.command "maybe [int%] ")
14 | (set self.doc "Runs a command sometimes (default 50% chance).")
15 | (set self.triggers (utilities.make_triggers bot [] [:maybe true]))
16 | (values))
17 |
18 | :action (fn [self bot msg]
19 | (local (probability input)
20 | (: msg.text :match (f-str "^{bot.config.cmd_pat}maybe%s+(%d*)%%?%s*(.+)")))
21 | (if (not input)
22 | (do (utilities.send_plugin_help msg.chat.id msg.message_id bot.config.cmd_pat self) nil)
23 | (let [probability (or (tonumber probability) 50)]
24 | (when (< (* (math.random) 100) probability)
25 | (set msg.text (anise.trim input))
26 | (: bot :on_message msg)))))
27 | }
28 |
--------------------------------------------------------------------------------
/otouto/plugins/user/nickname.fnl:
--------------------------------------------------------------------------------
1 | ;; nickname.fnl
2 | ;; Allows a user to set or delete his nickname.
3 |
4 | ;; Copyright 2019 topkecleon
5 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (local utilities (require :otouto.utilities))
8 |
9 | {
10 | :init (fn [self bot]
11 | (set self.command "nick ")
12 | (set self.doc "Set a nickname. Pass --
to delete it.")
13 | (set self.triggers (utilities.make_triggers bot [] [:nick true]))
14 | (if (not bot.database.userdata.nickname)
15 | (set bot.database.userdata.nickname {}))
16 | (set self.db bot.database.userdata.nickname)
17 | (values))
18 |
19 | ; No input returns nickname, or info if one isn't set.
20 | ; "--" or an em dash deletes a nickname.
21 | :action (fn [_ _ msg _ user]
22 | (utilities.send_reply msg (let [input (utilities.input msg.text)]
23 | (if (not input)
24 | (if user.data.nickname
25 | (.. "Your nickname is " user.data.nickname ".")
26 | ; else
27 | "You have no nickname.")
28 | ; else if input is '--' or em dash
29 | (or (= input "--") (= input utilities.char.em_dash))
30 | ; Delete the nickname.
31 | (do (set user.data.nickname nil)
32 | "Your nickname has been deleted.")
33 | ; else if input length > 32
34 | (> (utilities.utf8_len input) 32)
35 | "Nicknames cannot exceed 32 characters."
36 | ; else
37 | (let [nick (: input :gsub "\n" "")]
38 | (set user.data.nickname nick)
39 | (.. "Your nickname is now " nick "."))))))
40 | }
--------------------------------------------------------------------------------
/otouto/plugins/user/reactions.fnl:
--------------------------------------------------------------------------------
1 | ;; reactions.fnl
2 | ;; Provides a list of callable emoticons for the poor souls who don't have a
3 | ;; compose key.
4 |
5 | ;; Copyright 2016 topkecleon
6 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 |
8 | (require-macros :anise.macros)
9 | (require* otouto.utilities)
10 |
11 | {
12 | :init (fn [self bot]
13 | (set self.command "reactions")
14 | (set self.doc "Returns a list of \"reaction\" emoticon commands.")
15 | (set self.triggers (utilities.make_triggers bot [] :reactions))
16 |
17 | (local cmd_pat bot.config.cmd_pat)
18 | (local username (: bot.info.username :lower))
19 | ; Generate a command list message triggered by "/reactions".
20 | (set self.help "Reactions:\n")
21 | (each [trigger reaction (pairs bot.config.reactions)]
22 | (set self.help (f-str "{self.help}• {cmd_pat}{trigger}: {reaction}\n"))
23 | (table.insert self.triggers (f-str "^{cmd_pat}{trigger}"))
24 | (table.insert self.triggers (f-str "^{cmd_pat}{trigger}@{username}"))
25 | (table.insert self.triggers (f-str "{cmd_pat}{trigger}$"))
26 | (table.insert self.triggers (f-str "{cmd_pat}{trigger}@{username}$"))
27 | (table.insert self.triggers (f-str "\n{cmd_pat}{trigger}"))
28 | (table.insert self.triggers (f-str "\n{cmd_pat}{trigger}@{username}"))
29 | (table.insert self.triggers (f-str "{cmd_pat}{trigger}\n"))
30 | (table.insert self.triggers (f-str "{cmd_pat}{trigger}@{username}\n"))))
31 |
32 | :action (fn [self bot msg]
33 | (local cmd_pat bot.config.cmd_pat)
34 | (if (string.match msg.text_lower (f-str "{cmd_pat}reactions"))
35 | (do (utilities.send_message msg.chat.id self.help true nil :html) nil)
36 | (each [trigger reaction (pairs bot.config.reactions)]
37 | (when (string.match msg.text_lower (.. cmd_pat trigger))
38 | (utilities.send_message msg.chat.id reaction true nil :html)))))
39 | }
40 |
--------------------------------------------------------------------------------
/otouto/plugins/user/regex.fnl:
--------------------------------------------------------------------------------
1 | ;; regex.fnl
2 | ;; Sed-like substitution using PCRE regular expressions. Ignores commands with
3 | ;; no reply-to message.
4 |
5 | ;; Copyright 2017 topkecleon
6 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 |
8 | (require-macros :anise.macros)
9 | (require* anise
10 | re
11 | (rename rex_pcre rex)
12 | otouto.utilities)
13 |
14 | (local invoke_pattern (re.compile "\
15 | invocation <- 's/' {~ pcre ~} '/' {~ repl ~} ('/' modifiers)? !.\
16 | pcre <- ( [^\\/] / f_slash / '\\' )*\
17 | repl <- ( [^\\/%] / percent / f_slash / capture / '\\' )*\
18 | \
19 | modifiers <- { flags? } {~ n_matches? ~} {~ probability? ~}\
20 | \
21 | flags <- ('i' / 'm' / 's' / 'x' / 'U' / 'X')+\
22 | n_matches <- ('#' {[0-9]+}) -> '%1'\
23 | probability <- ('%' {[0-9]+}) -> '%1'\
24 | \
25 | f_slash <- ('\\' '/') -> '/'\
26 | percent <- '%' -> '%%%%'\
27 | capture <- ('\\' {[0-9]+}) -> '%%%1'\
28 | "))
29 |
30 | (fn process_text [raw_text cmd_pat]
31 | (local text (: raw_text :match (f-str "^{cmd_pat}?(.*)$")))
32 | (if (not text)
33 | nil
34 | (let [(patt repl flags n_matches probability) (: invoke_pattern :match text)]
35 | (if (not patt)
36 | nil
37 | (values
38 | patt
39 | repl
40 | flags
41 | (and n_matches (tonumber n_matches))
42 | (and probability (tonumber probability)))))))
43 |
44 | (fn make_n_matches [n_matches probability]
45 | (if
46 | (not probability)
47 | n_matches
48 | (not n_matches)
49 | (fn [] (< (* (math.random) 100) probability))
50 | ; else
51 | (do
52 | (var matches_left n_matches)
53 | (fn []
54 | (local tmp (and-or (< matches_left 0) 0 nil))
55 | (set matches_left (- matches_left 1))
56 | (values
57 | (< (* (math.random) 100) probability)
58 | tmp)))))
59 |
60 | {
61 | :init (fn [self bot]
62 | (set self.command "s//")
63 | (set self.help_word :regex)
64 | (set self.doc "Replace all matches for the given pattern.\n\z
65 | Uses PCRE regexes.\n\z
66 | \n\z
67 | Modifiers are [<flags>][#<matches>][%probability]:\n\z
68 | * Flags are i, m, s, x, U, and X, as per PCRE\n\z
69 | * Matches is how many matches to replace\n\z
70 | (all matches are replaced by default)\n\z
71 | * Probability is the percentage that a match will\n\z
72 | be replaced (100 by default)")
73 | (set self.triggers [(.. bot.config.cmd_pat "?s/.-/.-$")])
74 | (local flags_plugin (. bot.named_plugins :admin.flags))
75 | (set self.flag :regex_unwrapped)
76 | (set self.flag_desc "Regex substitutions aren't prefixed.")
77 | (when flags_plugin
78 | (tset flags_plugin.flags self.flag self.flag_desc))
79 | (values))
80 |
81 | :action (fn [self bot msg group]
82 | (if (not msg.reply_to_message)
83 | true
84 | (let [(patt repl flags n_matches probability) (process_text msg.text bot.config.cmd_pat)
85 | n_matches (make_n_matches n_matches probability)]
86 | (if (not patt)
87 | nil
88 | (let [input msg.reply_to_message.text
89 | input (and-or (= msg.reply_to_message.from.id bot.info.id)
90 | (: input :match "^Did you mean:\n\"(.+)\"$")
91 | input)
92 | (success result n_matched)
93 | (pcall (fn [] (rex.gsub input patt repl n_matches flags)))]
94 | (if (= success false)
95 | (let [output (.. "Malformed pattern!\n" (utilities.html_escape result))]
96 | (utilities.send_reply msg output)
97 | nil)
98 | (= n_matched 0)
99 | nil
100 | ; else
101 | (do (var output (anise.trim (: result :sub 1 4000)))
102 | (set output (utilities.html_escape output))
103 | (when (not (and flags_plugin (. group.data.admin.flags self.flag)))
104 | (set output (f-str "Did you mean:\n\"{}\"" output)))
105 | (utilities.send_reply msg.reply_to_message output :html)
106 | nil)))))))
107 | }
108 |
--------------------------------------------------------------------------------
/otouto/plugins/user/reminders.fnl:
--------------------------------------------------------------------------------
1 | ; reminders.fnl
2 | ; Copyright 2018 topkecleon
3 | ; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
4 |
5 | (require-macros :anise.macros)
6 | (require* otouto.utilities
7 | otouto.autils)
8 |
9 | {
10 | :init (fn [self bot]
11 | (set self.command "remind ")
12 | (set self.triggers (utilities.make_triggers bot []
13 | [:remind true] [:remindme true] [:reminder true]))
14 | (set self.doc "Set a reminder. The interval may be a number of seconds, \z
15 | or a tiem string, eg 3d12h30m
(see /help tiem). \z
16 | Reminders support HTML formatting. The text of a replied-to message \z
17 | can be made a reminder, if a valid interval follows the command. \n\z
18 | Example: /remind 4h Look at cat pics. \n\z
19 | Aliases: /remindme, /reminder.")
20 | (values))
21 |
22 | ; rmdr {
23 | ; :user {:id 55994550 :first_name :Drew}
24 | ; :chat_id -8675309
25 | ; :text "Post a cat pic."
26 | ; :date 1523000000}
27 | :later (fn [_self bot rmdr]
28 | (var output (f-str "Reminder from {} (UTC):\n{rmdr.text}"
29 | (os.date "!%F %T" rmdr.date)))
30 | (utilities.send_message rmdr.chat_id (if (~= rmdr.from.id rmdr.chat_id)
31 | (.. (utilities.lookup_name bot rmdr.from.id rmdr.from) "\n" output)
32 | ;else
33 | output)
34 | nil nil :html))
35 |
36 | :action (fn [self bot msg] (let [input (utilities.input msg.text)]
37 | (local output (if input (do
38 | (var (text interval) (autils.duration_from_reason input))
39 |
40 | ; text is message text and/or replied-to text, or nil.
41 | (set text (if (and msg.reply_to_message (> (# msg.reply_to_message.text) 0))
42 | (if text (.. msg.reply_to_message.text "\n\n" text)
43 | ;else
44 | msg.reply_to_message.text)
45 | text text
46 | ;else
47 | nil))
48 |
49 | (if (not interval) "Please specify a valid interval. See /help tiem."
50 | (not text) "Please specify text for the reminder."
51 | ;else
52 | (do (: bot :do_later self.name (+ (os.time) interval) {
53 | :from msg.from
54 | :chat_id msg.chat.id
55 | :text text
56 | :date msg.date})
57 | (.. "I will remind you in " (utilities.tiem.print interval) ":\n"
58 | (utilities.html_escape text)))))
59 |
60 | ; else (if not input)
61 | (utilities.plugin_help bot.config.cmd_pat self)))
62 |
63 | (utilities.send_reply msg output :html)))
64 | }
65 |
--------------------------------------------------------------------------------
/otouto/plugins/user/shout.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | shout.lua
3 | Returns an obnoxious shout.
4 |
5 | Copyright 2016 topkecleon
6 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 | ]]--
8 |
9 | local utilities = require('otouto.utilities')
10 | local anise = require('extern.anise')
11 |
12 | local P = {}
13 |
14 | function P:init(bot)
15 | self.command = 'shout '
16 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat):t('shout', true).table
17 | self.doc = 'Shouts something. Input may be the replied-to message.'
18 | self.utf8_char = '('..utilities.char.utf_8..'*)'
19 | end
20 |
21 | function P:action(bot, msg)
22 | local input = utilities.input_from_msg(msg)
23 | if input then
24 | input = anise.trim(input):upper():gsub("\n", " ")
25 | local output = ''
26 | local inc = 0
27 | local ilen = 0
28 | for match in input:gmatch(self.utf8_char) do
29 | if ilen < 20 then
30 | ilen = ilen + 1
31 | output = output .. match .. ' '
32 | end
33 | end
34 | ilen = 0
35 | output = output .. '\n'
36 | for match in input:sub(2):gmatch(self.utf8_char) do
37 | if ilen < 19 then
38 | local spacing = ''
39 | for _ = 1, inc do
40 | spacing = spacing .. ' '
41 | end
42 | inc = inc + 1
43 | ilen = ilen + 1
44 | output = output .. match .. ' ' .. spacing .. match .. '\n'
45 | end
46 | end
47 | output = '' .. anise.trim(output) .. '
'
48 | utilities.send_message(msg.chat.id, output, true, false, 'html')
49 | else
50 | utilities.send_reply(msg, self.doc, 'html')
51 | end
52 | end
53 |
54 | return P
55 |
56 |
--------------------------------------------------------------------------------
/otouto/plugins/user/slap.fnl:
--------------------------------------------------------------------------------
1 | ;; slap.fnl
2 | ;; Allows users to slap someone.
3 |
4 | ;; Copyright 2019 topkecleon
5 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (require-macros :anise.macros)
8 | (require* otouto.utilities
9 | (rename otouto.plugins.user.data.slap data))
10 |
11 | {
12 | :init (fn [self bot]
13 | (set self.command "slap [target]")
14 | (set self.doc "Slap somebody.")
15 | (set self.triggers (utilities.make_triggers bot [] [:slap true]))
16 | (values))
17 |
18 | :action (fn [self bot msg]
19 | (local victor (utilities.get_nick bot msg.from))
20 | (local input (utilities.input msg.text))
21 | (local users bot.database.userdata.info)
22 | (local victim (if msg.reply_to_message
23 | (utilities.get_nick bot msg.reply_to_message.from)
24 | input
25 | (if (= (: input :match "^@(.+)$") bot.info.username)
26 | bot.info.first_name
27 | (: input :match "^@.")
28 | (let [user (utilities.resolve_username bot input)]
29 | (if user (utilities.get_nick bot user) input))
30 | (and (tonumber input) users users[input])
31 | (utilities.get_nick bot users[input])
32 | ; else
33 | input)
34 | ; else
35 | (utilities.get_nick bot msg.from)))
36 | (local victor (if (= victor victim) bot.info.first_name victor))
37 |
38 | (let [victor (: victor :gsub "%%" "%%%%")
39 | victim (: victim :gsub "%%" "%%%%")
40 | slap (.. utilities.char.zwnj (. data (math.random (# data))))
41 | output (: (: slap :gsub "VICTOR" victor) :gsub "VICTIM" victim)]
42 | (utilities.send_message msg.chat.id output)
43 | nil))
44 | }
45 |
--------------------------------------------------------------------------------
/otouto/plugins/user/urban_dictionary.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | urban_dictionary.lua
3 | Returns Urban Dictionary definitions. Now featuring paging and links!
4 |
5 | Copyright 2019 topkecleon
6 | This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 | ]]--
8 |
9 | local json = require('dkjson')
10 | local url = require('socket.url')
11 | local https = require('ssl.https')
12 |
13 | local bindings = require('extern.bindings')
14 | local utilities = require('otouto.utilities')
15 | local anise = require('extern.anise')
16 |
17 | local P = {}
18 |
19 | local three_hours = 60 * 60 * 3
20 |
21 | function P:init(bot)
22 | self.command = 'urbandictionary [query]'
23 | self.doc = string.format(
24 | 'Search the Urban Dictionary.\nAliases: %sud, %surban\n\zBy default, \z
25 | results include an inline keyboard for paging and related terms. Use \z
26 | %sud1 for only paging buttons. Use %sud0 for no keyboard.',
27 | bot.config.cmd_pat,
28 | bot.config.cmd_pat,
29 | bot.config.cmd_pat,
30 | bot.config.cmd_pat
31 | )
32 | self.triggers = utilities.triggers(bot.info.username, bot.config.cmd_pat)
33 | :t('ud%d?', true)
34 | :t('urbandictionary', true)
35 | :t('urban', true).table
36 |
37 | self.kb_level_pattern = '^' .. bot.config.cmd_pat .. 'ud(%d)'
38 |
39 | -- Cache to store definition lists.
40 | -- Should provide a noticeable speed benefit for paging.
41 | self.cache = {}
42 |
43 | -- Schedule a job to clear expired cached lists.
44 | bot:do_later(self.name, os.time() + three_hours)
45 |
46 | self.url = 'http://api.urbandictionary.com/v0/define?term='
47 | end
48 |
49 | function P:action(bot, msg)
50 | local query = utilities.input_from_msg(msg)
51 | if query then
52 | local list = self:fetch(url.escape(query))
53 | if list ~= false then
54 | if #list > 0 then
55 | local kb_level = tonumber(msg.text:lower():match(self.kb_level_pattern) or 2)
56 | local message = self:create_message(list, 1, kb_level)
57 | message.chat_id = msg.chat.id
58 | message.reply_to_message_id = msg.message_id
59 | bindings.sendMessage(message)
60 | else
61 | utilities.send_reply(msg, bot.config.errors.results)
62 | end
63 | else
64 | utilities.send_reply(msg, bot.config.errors.connection)
65 | end
66 | else
67 | utilities.send_plugin_help(msg.chat.id, msg.message_id, bot.config.cmd_pat, self)
68 | end
69 | end
70 |
71 | function P:callback_action(_, query)
72 | local escaped_term = utilities.get_word(query.data, 2)
73 | local def_num = tonumber(utilities.get_word(query.data, 3))
74 | local kb_level = tonumber(utilities.get_word(query.data, 4))
75 |
76 | local new_list = P:fetch(escaped_term)
77 | local new_message = self:create_message(new_list, def_num, kb_level)
78 | new_message.chat_id = query.message.chat.id
79 | new_message.message_id = query.message.message_id
80 | new_message.parse_mode = 'html'
81 | new_message.disable_web_page_preview = true
82 |
83 | bindings.editMessageText(new_message)
84 | end
85 |
86 | -- Clear cached results which have expired.
87 | function P:later(bot)
88 | for term, tab in pairs(self.cache) do
89 | if tab.expires < os.time() then
90 | self.cache[term] = nil
91 | end
92 | end
93 | -- Schedule another check in three hours.
94 | bot:do_later(self.name, os.time() + three_hours)
95 | end
96 |
97 | -- Fetch a list of definitions from the Urban Dictionary API or the cache.
98 | function P:fetch(escaped_term)
99 | escaped_term = escaped_term:lower()
100 | -- If the term is cached, use that.
101 | if self.cache[escaped_term] then
102 | -- Reset the expiration date.
103 | self.cache[escaped_term].expires = os.time() + three_hours
104 | return self.cache[escaped_term].list
105 | else
106 | -- Otherwise, check the API.
107 | local jstr, response = https.request(self.url .. escaped_term)
108 | if response == 200 then
109 | local data = json.decode(jstr)
110 |
111 | -- Cache the results.
112 | self.cache[escaped_term] = {
113 | expires = os.time() + three_hours,
114 | list = data.list
115 | }
116 |
117 | return data.list
118 | else
119 | return false
120 | end
121 | end
122 | end
123 |
124 | -- i is the index of the entry which should be display.
125 | -- kb_level: 0 is no keyboard. 1 is paging keyboard. 2 is full keyboard.
126 | -- Paging keyboard provides arrows to navigate through several entries for
127 | -- one term. Full keyboard provides arrows as well as keys for related terms
128 | -- which are linked in the definition.
129 | function P:create_message(list, i, kb_level)
130 | local entry = list[i]
131 | local message = {
132 | parse_mode = 'html',
133 | disable_web_page_preview = true,
134 | text = self.format_entry(list, i)
135 | }
136 |
137 | if kb_level > 0 then
138 | -- Initialize the keyboard.
139 | local keyboard = utilities.keyboard('inline_keyboard')
140 |
141 | -- Paging arrows if there is more than one entry for the term.
142 | if #list > 1 then
143 | local prev_entry_num = list[i - 1] and i - 1 or #list
144 | local next_entry_num = list[i + 1] and i + 1 or 1
145 | local escaped_term = url.escape(entry.word)
146 |
147 | keyboard:row()
148 | keyboard:button('◀️', 'callback_data', string.format(
149 | '%s %s %s %s',
150 | self.name,
151 | escaped_term,
152 | prev_entry_num,
153 | kb_level
154 | ))
155 | keyboard:button('▶️', 'callback_data', string.format(
156 | '%s %s %s %s',
157 | self.name,
158 | escaped_term,
159 | next_entry_num,
160 | kb_level
161 | ))
162 | end
163 |
164 | if kb_level > 1 then
165 | -- Set for all terms appearing in the definition or example, which will be on buttons on the keyboard.
166 | local linked_terms = anise.set()
167 | -- Iterate over bracketed terms in the definition.
168 | for term in entry.definition:gmatch('%[(.-)%]') do
169 | -- Don't add a term if it's the same as the initial term.
170 | if term:lower() ~= entry.word:lower() then
171 | linked_terms:add(term:lower())
172 | end
173 | end
174 | -- Iterate over bracketed terms in the example.
175 | for term in entry.example:gmatch('%[(.-)%]') do
176 | if term:lower() ~= entry.word:lower() then
177 | linked_terms:add(term:lower())
178 | end
179 | end
180 |
181 | -- Populate with linked terms.
182 | local j = 0
183 | for term in pairs(linked_terms) do
184 | -- When there are three buttons in a row, a new row is started.
185 | if j % 3 == 0 then
186 | keyboard:row()
187 | end
188 | keyboard:button(term, 'callback_data', string.format(
189 | '%s %s %s %s',
190 | self.name,
191 | url.escape(term),
192 | 1,
193 | kb_level
194 | ))
195 | j = j + 1
196 | end
197 | end
198 |
199 | -- Prep the functions
200 | message.reply_markup = keyboard:serialize()
201 | end
202 |
203 | return message
204 | end
205 |
206 | function P.format_entry(list, i)
207 | local entry = list[i]
208 | local definition = entry.definition:gsub('%[(.-)%]', '%1')
209 | local text = string.format(
210 | '%s (%s of %s)\n%s\n\n%s',
211 | entry.word,
212 | entry.permalink,
213 | i,
214 | #list,
215 | entry.written_on:sub(1, 10),
216 | utilities.html_escape(definition)
217 | )
218 | if entry.example then
219 | local example = entry.example:gsub('%[(.-)%]', '%1')
220 | text = text .. '\n\n' .. utilities.html_escape(example) .. ''
221 | end
222 | return text
223 | end
224 |
225 | return P
226 |
--------------------------------------------------------------------------------
/otouto/plugins/user/user_lookup.fnl:
--------------------------------------------------------------------------------
1 | ;; user_lookup.fnl
2 | ;; Returns cached user info, if any, for the given targets.
3 |
4 | ;; Copyright 2018 topkecleon
5 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (require-macros :anise.macros)
8 | (require* anise
9 | otouto.utilities
10 | otouto.autils)
11 |
12 | {
13 | :init (fn [self bot]
14 | (set self.command "lookup")
15 | (set self.doc "Returns stored user info, if any, for the given users.")
16 | (set self.triggers (utilities.make_triggers bot [] [:lookup true]))
17 | (set self.targeting true)
18 | (values))
19 |
20 | :action (fn [self bot msg]
21 | (local (targets output)
22 | (autils.targets bot msg {:unknown_ids_err true :self_targeting true}))
23 | (anise.pushcat output (utilities.list_names bot targets))
24 | (utilities.send_reply msg (table.concat output "\n") :html)
25 | nil)
26 | }
27 |
--------------------------------------------------------------------------------
/otouto/plugins/user/whoami.fnl:
--------------------------------------------------------------------------------
1 | ;; whoami.fnl
2 | ;; Returns the user's or replied-to user's display name, username, and ID, in
3 | ;; addition to the group's display name, username, and ID.
4 |
5 | ;; Copyright 2018 topkecleon
6 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
7 |
8 | (require-macros :anise.macros)
9 | (require* otouto.utilities)
10 |
11 | {
12 | :init (fn [self bot]
13 | (set self.command "who")
14 | (set self.doc "Returns user and chat info for you or the replied-to message.")
15 | (set self.triggers (utilities.make_triggers bot [] :who :whoami))
16 | (values))
17 |
18 | :action (fn [self bot msg]
19 | (let [; Operate on the replied-to message, if there is one.
20 | msg (or msg.reply_to_message msg)
21 | ; If it's a private conversation, bot is chat, unless bot is from.
22 | chat (if (= msg.from.id msg.chat.id) bot.info msg.chat)
23 | new_or_left (or msg.new_chat_member msg.left_chat_member)
24 | output (if new_or_left
25 | (f-str "{} {} {} {} {}."
26 | (utilities.format_name msg.from)
27 | (if msg.new_chat_member "added" "removed")
28 | (utilities.format_name new_or_left)
29 | (if msg.new_chat_member "to" "from")
30 | (utilities.format_name chat))
31 | ; else
32 | (f-str "You are {}, and you are messaging {}."
33 | (utilities.format_name msg.from)
34 | (utilities.format_name chat)))]
35 | (utilities.send_message msg.chat.id output true msg.message_id :html)
36 | nil))
37 | }
38 |
--------------------------------------------------------------------------------
/otouto/plugins/user/wikipedia.fnl:
--------------------------------------------------------------------------------
1 | ;; wikipedia.fnl
2 | ;; Returns a Wikipedia result for a given query.
3 |
4 | ;; Copyright 2018 topkecleon
5 | ;; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
6 |
7 | (require-macros :anise.macros)
8 | (require* ssl.https
9 | socket.url
10 | (rename dkjson json)
11 | otouto.utilities)
12 |
13 | {
14 | :init (fn [self bot]
15 | (set self.command "wikipedia ")
16 | (set self.doc
17 | "Returns an article from Wikipedia.\n\z
18 | Aliases: /w, /wiki")
19 | (set self.triggers (utilities.make_triggers bot []
20 | [:wikipedia true] [:wiki true] [:w true]))
21 | (set self.search_url (.. "https://" bot.config.lang ".wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch="))
22 | (set self.result_url (.. "https://" bot.config.lang ".wikipedia.org/w/api.php?action=query&format=json&prop=extracts&exchars=4000&explaintext=&titles="))
23 | (set self.article_url (.. "https://" bot.config.lang ".wikipedia.org/wiki/")))
24 |
25 | :get_title (fn [dsq] (let [t []]
26 | (each [_ v (pairs dsq)]
27 | (if (not (: v.snippet :match "may refer to"))
28 | (table.insert t v.title)))
29 | (if (> (# t) 0) (. t 1) false)))
30 |
31 | :build (fn [self title text]
32 | (f-str "{}\n{}\nRead more."
33 | (utilities.html_escape title)
34 | (let [l (: text :find "\n")]
35 | (if l (: text :sub 1 (- l 1)) text))
36 | (.. self.article_url (url.escape title)))
37 | )
38 |
39 | :action (fn [self bot msg] (let [input (utilities.input_from_msg msg)]
40 | (let [output
41 | (if (not input) self.doc
42 | (let [(res code) (https.request (.. self.search_url (url.escape input)))]
43 | (if (~= code 200) bot.config.errors.connection
44 | (let [data (json.decode res)]
45 | (if (or (not data.query) (= data.query.searchinfo.totalhits 0)) bot.config.errors.results
46 | (let [title (self.get_title data.query.search)]
47 | (if (not title) bot.config.errors.results
48 | (let [(res code) (https.request (.. self.result_url (url.escape title)))]
49 | (if (~= code 200) bot.config.errors.connection
50 | (let [(_ text) (next (. (json.decode res) :query :pages))]
51 | (if (not text) bot.config.errors.results
52 | (: self :build title text.extract))))))))))))]
53 | (utilities.send_reply msg output :html)))
54 | nil)
55 | }
56 |
--------------------------------------------------------------------------------
/otouto/plugins/user/xkcd.fnl:
--------------------------------------------------------------------------------
1 | ; xkcd.fnl
2 | ; Copyright 2018 topkecleon
3 | ; This code is licensed under the GNU AGPLv3. See /LICENSE for details.
4 |
5 | (require-macros :anise.macros)
6 | (require* (rename dkjson json)
7 | ssl.https
8 | extern.bindings
9 | otouto.utilities)
10 |
11 | {
12 | :init (fn [self bot]
13 | (set self.command "xkcd [i]")
14 | (set self.doc "Returns the latest xkcd strip or a specified one. \
15 | i may be \"r\" or \"random\" for a random strip.")
16 | (set self.triggers (utilities.make_triggers bot [] [:xkcd true]))
17 |
18 | (when (not bot.database.xkcd) (: self :later bot)))
19 |
20 | :later (fn [self bot]
21 | (: bot :do_later self.name (+ (os.time) 21600))
22 | (local (res code) (https.request "https://xkcd.com/info.0.json"))
23 | (when (= code 200)
24 | (set bot.database.xkcd (json.decode res))))
25 |
26 | :action (fn [self bot msg] (let [input (utilities.get_word msg.text 2)
27 | (res code) (https.request (f-str "https://xkcd.com/{}/info.0.json"
28 | (if (or (= input :r) (= input :random))
29 | (math.random bot.database.xkcd.num)
30 | (tonumber input)
31 | input
32 | ;else
33 | (tostring bot.database.xkcd.num))))]
34 | (when (= code 200) (let [strip (json.decode res)]
35 | ; Simple way to correct an out-of-date latest strip.
36 | (when (> strip.num bot.database.xkcd.num) (set bot.database.xkcd strip))
37 |
38 | (bindings.sendPhoto {
39 | :chat_id msg.chat.id
40 | :parse_mode :html
41 | :photo strip.img
42 | :caption (f-str "{}\n{}\nhttps://xkcd.com/{}"
43 | (utilities.html_escape (utilities.fix_utf8 strip.safe_title))
44 | (utilities.html_escape strip.alt)
45 | strip.num
46 | )
47 | })))))
48 | }
49 |
--------------------------------------------------------------------------------
/otouto/rot13.fnl:
--------------------------------------------------------------------------------
1 | ;; https://github.com/kennyledet/Algorithm-Implementations/blob/master/ROT13_Cipher/Lua/Yonaba/rot13.lua
2 | ;; MIT licensed
3 |
4 | ;; ROT13 ciphering algorithm implementation
5 | ;; See: http://en.wikipedia.org/wiki/ROT13
6 |
7 | ;; Returns the ASCII bytecode of either 'a' or 'A'
8 | (local a_byte (string.byte :a))
9 | (local A_byte (string.byte :A))
10 | (local ascii_base (fn [ch]
11 | (if (= (string.lower ch) ch) a_byte A_byte)))
12 |
13 | (local caesar_cipher_ch (fn [key] (fn [ch]
14 | (let [base (ascii_base ch)
15 | offset (% (+ (- (string.byte ch) base) key) 26)]
16 | (string.char (+ offset base))))))
17 |
18 | ;; ROT13 is based on Caesar ciphering algorithm, using 13 as a key
19 | (local caesar_cipher (fn [key str]
20 | (string.gsub str "%a" (caesar_cipher_ch key))))
21 |
22 | {
23 | :cipher (partial caesar_cipher 13)
24 |
25 | :decipher (partial caesar_cipher -13)
26 | }
27 |
--------------------------------------------------------------------------------
/shell_preamble.fnl:
--------------------------------------------------------------------------------
1 | (eval-compiler (dofile :fennel_preamble.lua))
2 |
--------------------------------------------------------------------------------