├── MirahezeBots ├── __init__.py ├── plugins │ ├── __init__.py │ ├── shared.py │ ├── miraheze.py │ ├── goofy.py │ ├── shortlinks.py │ ├── responses.py │ ├── welcome.py │ ├── status.py │ └── phab.py ├── tests │ ├── __init__.py │ ├── test_general.py │ └── models.py ├── utils │ ├── __init__.py │ ├── phabapi.py │ └── mwapihandler.py ├── example.db ├── version.py └── dbclean.py ├── .github ├── merge-when-green.yml ├── dependabot.yml └── workflows │ ├── dependabot-merge.yml │ ├── codeql-analysis.yml │ └── workflow.yml ├── MANIFEST.in ├── .flake8 ├── .gitignore ├── .deepsource.toml ├── compatibility.txt ├── requirements.txt ├── sonar-project.properties ├── dev-requirements.txt ├── README.md ├── SECURITY.md ├── LICENSE ├── setup.cfg ├── setup.py └── CHANGELOG.md /MirahezeBots/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /MirahezeBots/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /MirahezeBots/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /MirahezeBots/utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /MirahezeBots/example.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FOSSBots/MirahezeBots/HEAD/MirahezeBots/example.db -------------------------------------------------------------------------------- /.github/merge-when-green.yml: -------------------------------------------------------------------------------- 1 | requiredChecks: 2 | - github-actions 3 | - github-code-scanning 4 | - lgtm-com 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | include README.md 3 | include *requirements.txt 4 | 5 | recursive-exclude * __pycache__ 6 | recursive-exclude * *.py[co] 7 | -------------------------------------------------------------------------------- /MirahezeBots/version.py: -------------------------------------------------------------------------------- 1 | """Provides version information.""" 2 | VERSION = '9.1.5' 3 | VERSIONARRAY = VERSION.split('.') 4 | SHORTVERSION = 'v' + str(VERSIONARRAY[0]) 5 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 125 3 | exclude = */__init__.py, dist/*, build/* 4 | extend-ignore = SFS301, SIM114 5 | per-file-ignores = 6 | MirahezeBots/utils/phabapi.py:SFS201 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | build/* 3 | build 4 | dist 5 | __pycache__ 6 | __pycache__/* 7 | .cache 8 | *.pyc 9 | .idea 10 | .github/ 11 | .DS_Store 12 | */.DS_Store 13 | hasan2.db 14 | MirahezeBot_Plugins.egg-info/* 15 | MirahezeBot_Plugins.egg-info 16 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = ["MirahezeBots/tests/*.py"] 4 | 5 | [[analyzers]] 6 | name = "python" 7 | enabled = true 8 | 9 | [analyzers.meta] 10 | runtime_version = "3.x.x" 11 | 12 | [[transformers]] 13 | name = "autopep8" 14 | enabled = false 15 | 16 | [[transformers]] 17 | name = "isort" 18 | enabled = false 19 | -------------------------------------------------------------------------------- /compatibility.txt: -------------------------------------------------------------------------------- 1 | sopel-help==0.4.0 2 | sopel-modules.quotes==1.2.1 3 | sopel-modules.twitter==0.4.1 4 | sopel-modules.weather==1.4.0 5 | sopel-modules.youtube==0.5.0 6 | sopel-modules.stocks==1.1.3 7 | sopel-modules.chanlogs==0.2.2 8 | sopel-modules.github==0.4.6 9 | emoji==2.0.0 10 | sopel-modules.wolfram==0.5.0 11 | sopel-modules.urban==1.2.1 12 | sopel-dns==0.3.1 13 | pipdeptree==2.3.1 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | open-pull-requests-limit: 10 12 | target-branch: dev 13 | labels: 14 | - "merge when green" 15 | - "dependencies" 16 | -------------------------------------------------------------------------------- /MirahezeBots/plugins/shared.py: -------------------------------------------------------------------------------- 1 | """Sopel Inter-Plugin resource sharing service.""" 2 | 3 | from requests import Session 4 | from sopel import bot 5 | from sopel.tools import SopelMemory 6 | 7 | 8 | def setup(instance: bot) -> None: 9 | """Create the resources that can be accessed via sopel shared.""" 10 | instance.memory['shared'] = SopelMemory() 11 | instance.memory['shared']['session'] = Session() 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # dependenices used in MirahezeBot 2 | sopel>=7.1.1,<9 3 | requests>=2.26.0,<2.27.0 4 | idna>=3.2,<4 5 | requests-cache==0.9.6 6 | # shared util scripts 7 | MirahezeBots-jsonparser>=1.0.4,<2.0.0 8 | # code migrated to own repo awaiting bundle switch 9 | wheel>=0.36.2,<0.38.0 10 | sopel-plugins.adminlist>=1.0.7 11 | sopel-plugins.channelmgnt>=2.1.1 12 | sopel-plugins.joinall>=1.0.4 13 | urllib3>=1.26.6 # avoid GHSA-5phf-pp7p-vc2r 14 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=MirahezeBots_MirahezeBots 2 | sonar.organization=mirahezebots 3 | sonar.pullrequest.github.summary_comment=True 4 | sonar.coverage.exclusions=**/** 5 | # This is the name and version displayed in the SonarCloud UI. 6 | #sonar.projectName=MirahezeBots 7 | #sonar.projectVersion=1.0 8 | 9 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 10 | #sonar.sources=. 11 | 12 | # Encoding of the source code. Default is default system encoding 13 | #sonar.sourceEncoding=UTF-8sonar-project.properties 14 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v1.3.3 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: gh pr merge --auto --squash "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # Development requirements 2 | pip-check-reqs==2.3.2 3 | setuptools==65.3.0 4 | pytest==7.1.3 5 | flake8==5.0.4 6 | flake8-docstrings==1.6.0 7 | #flake8-import-order==0.18.1 8 | flake8-unused-arguments==0.0.11 9 | flake8-sfs==0.0.3 10 | flake8-builtins==1.5.3 11 | flake8-commas==2.1.0 12 | flake8-comprehensions==3.10.0 13 | flake8-eradicate==1.3.0 14 | flake8-fixme==1.1.1 15 | flake8-multiline-containers==0.0.19 16 | flake8-print==5.0.0 17 | flake8-pytest-style==1.6.0 18 | flake8-return==1.1.3 19 | flake8-quotes==3.3.1 20 | flake8-simplify==0.19.3 21 | flake8-pytest==1.4 22 | pipdeptree==2.3.1 23 | packaging>=20.8 24 | build==0.8.0 25 | SQLAlchemy>=1.4.13,<1.5.0 26 | bandit==1.7.4 27 | mypy==0.971 28 | types-requests==2.28.10 29 | sqlalchemy-stubs==0.4 30 | -------------------------------------------------------------------------------- /MirahezeBots/dbclean.py: -------------------------------------------------------------------------------- 1 | """Remove a list of users from sopel database.""" 2 | from sqlite3 import connect 3 | 4 | 5 | def rundel() -> None: 6 | """Attempt the actual function for the cli script.""" 7 | file = input('Full path to the deletion list: ') 8 | with open(file, 'r') as f: # ensure the file is open and closed properly 9 | users = f.readlines() 10 | database = input('Full path to database: ') 11 | with connect(database) as conn: 12 | curs = conn.cursor() 13 | for user in users: 14 | curs.execute('DELETE FROM nick_values WHERE nick_id = ?', (user,)) 15 | curs.execute('DELETE FROM nicknames WHERE nick_id = ?', (user,)) 16 | curs.execute('DELETE FROM nick_ids WHERE nick_id = ?', (user,)) 17 | conn.commit() 18 | 19 | 20 | if __name__ == '__main__': 21 | rundel() 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MirahezeBots 2 | 3 | MirahezeBots is an IRC bot mainly used by [Miraheze](https://meta.miraheze.org) 4 | ([#miraheze on Libera.Chat](http://web.libera.chat)). 5 | 6 | It is simple and easy to customize. 7 | MirahezeBot uses [Sopel](https://sopel.chat). 8 | 9 | To use this bot, simply type install 'MirahezeBot-Plugins' from PyPi and then run 'sopel configure --plugins' 10 | 11 | To make use of the beta branch, clone the repo and check the 'dev' branch out and use pip's "install ." function to install it. You can then use 'sopel configure --plugins' as normal. 12 | 13 | 14 | Please note that in line with our security policy, we can only support Sopel 7.1.2 - 8.0 installations running Python 3.8+. 15 | 16 | [Source Github](http://github.com/sopel-irc/sopel) 17 | 18 | [More info](https://fossbots.org) 19 | 20 | [Documentation](https://fossbots.org/documentation.html) 21 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We currently support with security releases: the lastest PyPi version on the Release branch, and the Dev branch when running sopel 7.1.2 - 8.0 on python 3.8+. 6 | 7 | 8 | ## Reporting a Vulnerability 9 | 10 | 11 | To report a Security Vulnerability or issue, we ask that you please email any applicable information to: staff[at]fossbots.org. 12 | 13 | Please note that by doing this you agree to disclose your email address, and/or any information provided in the email header. 14 | 15 | At a minimum, we ask that you provide, context to the finding of the vulnerability/issue, steps to recreate the vulnerability/issue, and if you have a potiental solution. 16 | 17 | Please do note every effort will be done to respect your privacy, however, please do note the following: 18 | 19 | By using this project or contacting us, you agree that your details can be used in accordance with our Privacy Policy (https://fossbots.org/privacy.html). 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eiffel Forum License, version 2 2 | 3 | 1. Permission is hereby granted to use, copy, modify and/or 4 | distribute this package, provided that: 5 | * copyright notices are retained unchanged, 6 | * any distribution of this package, whether modified or not, 7 | includes this license text. 8 | 2. Permission is hereby also granted to distribute binary programs 9 | which depend on this package. If the binary program depends on a 10 | modified version of this package, you are encouraged to publicly 11 | release the modified version of this package. 12 | 13 | *********************** 14 | 15 | THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT WARRANTY. ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE TO ANY PARTY FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THIS PACKAGE. 21 | 22 | *********************** 23 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE 3 | classifiers = 4 | Development Status :: 5 - Production/Stable 5 | Intended Audience :: Developers 6 | Intended Audience :: System Administrators 7 | License :: Eiffel Forum License (EFL) 8 | License :: OSI Approved :: Eiffel Forum License 9 | Operating System :: POSIX :: Linux 10 | Operating System :: MacOS 11 | Programming Language :: Python :: 3.8 12 | Programming Language :: Python :: 3.9 13 | Topic :: Communications :: Chat :: Internet Relay Chat 14 | [options] 15 | python_requires = >=3.8 16 | [options.entry_points] 17 | sopel.plugins = 18 | miraheze = MirahezeBots.plugins.miraheze 19 | phab = MirahezeBots.plugins.phab 20 | responses = MirahezeBots.plugins.responses 21 | shortlinks = MirahezeBots.plugins.shortlinks 22 | status = MirahezeBots.plugins.status 23 | welcome = MirahezeBots.plugins.welcome 24 | goofy = MirahezeBots.plugins.goofy 25 | shared = MirahezeBots.plugins.shared 26 | console_scripts = 27 | sopel-dbclean = MirahezeBots.dbclean:rundel 28 | 29 | [mypy] 30 | plugins = sqlmypy 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Controls the setup of the package by setuptools/pip.""" 2 | from setuptools import find_packages, setup 3 | 4 | from MirahezeBots.version import VERSION 5 | 6 | with open('README.md') as readme_file: 7 | readme = readme_file.read() 8 | 9 | with open('CHANGELOG.md') as history_file: 10 | history = history_file.read() 11 | with open('requirements.txt') as requirements_file: 12 | requirements = list(requirements_file.readlines()) 13 | 14 | with open('dev-requirements.txt') as dev_requirements_file: 15 | dev_requirements = list(dev_requirements_file.readlines()) 16 | 17 | 18 | setup( 19 | name='MirahezeBot_Plugins', 20 | version=VERSION, 21 | description='Sopel Plugins for Miraheze Bots', 22 | long_description=readme + '\n\n' + history, 23 | long_description_content_type='text/markdown', # This is important! 24 | author='MirahezeBot Contributors', 25 | author_email='staff@fossbots.org', 26 | url='https://github.com/FOSSBots/MirahezeBots', 27 | packages=find_packages('.'), 28 | include_package_data=True, 29 | install_requires=requirements, 30 | tests_require=dev_requirements, 31 | test_suite='tests', 32 | license='Eiffel Forum License, version 2', 33 | ) 34 | -------------------------------------------------------------------------------- /MirahezeBots/tests/test_general.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | """General tests for all MHB plugins.""" 3 | import os 4 | import re 5 | import sqlite3 6 | import sys 7 | from contextlib import suppress 8 | 9 | from sqlalchemy import create_engine 10 | 11 | from MirahezeBots.tests import models 12 | 13 | PATH = '../MirahezeBots/MirahezeBots' 14 | PLUGINPATH = '../MirahezeBots/MirahezeBots/plugins' 15 | sys.path.append(PATH) 16 | 17 | 18 | def test_db_schema_is_same() -> None: 19 | """Confirms database matches as expected.""" 20 | original, new = set(), set() # noqa: F841 21 | with sqlite3.connect(os.path.join(PATH, 'example.db')) as conn: 22 | conn.text_factory = str 23 | res = conn.execute("SELECT name FROM sqlite_master WHERE type='table';") 24 | for tbl in res: 25 | if tbl[0] not in ('nick_ids', 'sqlite_sequence'): 26 | original.add(tbl[0]) 27 | with suppress(FileNotFoundError): 28 | os.unlink(os.path.join(PATH, 'example-model.db')) 29 | 30 | engine = create_engine(f"sqlite:///{os.path.join(PATH, '..', 'example-model.db')}") 31 | models.Base.metadata.create_all(bind=engine) 32 | assert original == set(engine.table_names()) 33 | 34 | 35 | def test_no_get_on_lists() -> None: 36 | """Checks for misuse of .get() on lists.""" 37 | reg = r'get\([0-9]' 38 | for top, dirs, files in os.walk(PLUGINPATH): 39 | for filen in files: 40 | if not filen.endswith('.py'): 41 | continue 42 | with open(os.path.join(PLUGINPATH, filen)) as python_source: 43 | src = python_source.read() 44 | assert not re.search(reg, src) 45 | 46 | 47 | def future_test_db_cleanup() -> None: 48 | """Confirms database matches as expected.""" # noqa: D401 49 | engine = create_engine(f'sqlite:///{os.path.join(PATH, "..", "hasan2.db")}') 50 | models.Base.metadata.create_all(bind=engine) 51 | -------------------------------------------------------------------------------- /MirahezeBots/plugins/miraheze.py: -------------------------------------------------------------------------------- 1 | """This plugin contains miraheze specific commands.""" 2 | from sopel.plugin import commands, example, rule 3 | from sopel import bot, trigger 4 | 5 | MIRAHEZE_ABOUT_MIRAHEZE_CHANNEL = ( 6 | 'Miraheze is a non-profit wikifarm running MediaWiki. If you would like ' 7 | 'more information please see ' 8 | 'https://meta.miraheze.org/ or ask in this channel.' 9 | ) 10 | MIRAHEZE_ABOUT_OTHER_CHANNELS = ( 11 | 'Miraheze is a non-profit wikifarm running MediaWiki. If you would like ' 12 | 'more information please see ' 13 | 'https://meta.miraheze.org/ or #miraheze.' 14 | ) 15 | 16 | 17 | @commands('miraheze') 18 | @rule('.*[w-wW-W]hat (even is [m-mM-M]iraheze|is [m-mM-M]iraheze|does [m-mM-M]iraheze do).*') 19 | @example('.miraheze') 20 | def miraheze(instance: bot, message: trigger) -> None: 21 | """Tells you about Miraheze and where to learn more.""" 22 | if message.sender == '#miraheze': 23 | instance.reply(MIRAHEZE_ABOUT_MIRAHEZE_CHANNEL) 24 | else: 25 | instance.reply(MIRAHEZE_ABOUT_OTHER_CHANNELS) 26 | 27 | 28 | @commands('gethelp') 29 | @rule("([i-iI-I] need help|[c-cC-C]an someone help me|[i-iI-I] can(t|'t) login).*") 30 | @example('.gethelp I cannot access https://meta.miraheze.org') 31 | def miraheze_gethelp(instance: bot, message: trigger) -> None: 32 | """Reply to help requests.""" 33 | if message.sender == '#miraheze': 34 | instance.reply( 35 | 'Pinging Agent, CosmicAlpha, dmehus, JohnLewis, paladox, Reception123, RhinosF1 and Voidwalker,' 36 | 'who might be able to help you. Other users in this channel also see this and may be able to assist you.') 37 | else: 38 | instance.reply('If you need Miraheze releated help, please join #miraheze') 39 | 40 | 41 | @commands('discord') 42 | def miraheze_discord(instance: bot, message: trigger) -> None: # noqa: U100 43 | """Display discord information for Miraheze.""" 44 | instance.reply('You can join discord by going to, https://miraheze.org/discord!') 45 | -------------------------------------------------------------------------------- /MirahezeBots/plugins/goofy.py: -------------------------------------------------------------------------------- 1 | """Some commands for just goofing around and having fun.""" 2 | 3 | from sopel.plugin import commands, example 4 | from sopel import bot, trigger 5 | 6 | 7 | @example('.coffee MirahezeBot') 8 | @commands('coffee') 9 | def coffee(instance: bot, message: trigger) -> None: 10 | """Make me give the specified nick a coffee.""" 11 | if message.group(2) is None: 12 | instance.reply('To whom should I give this cup of coffee?') 13 | else: 14 | instance.action(f'gives {message.group(2)} a nice warm cup of coffee.', message.sender) 15 | 16 | 17 | @example('.hug MirahezeBot') 18 | @commands('hug') 19 | def hug(instance: bot, message: trigger) -> None: 20 | """Make me give the specified nick a hug.""" 21 | if message.group(2) is None: 22 | instance.reply('To whom should I give this hug?') 23 | else: 24 | instance.action(f'gives {message.group(2)} a great big bear hug.', message.sender) 25 | 26 | 27 | @example('.burger MirahezeBot') 28 | @commands('burger') 29 | def burger(instance: bot, message: trigger) -> None: 30 | """Make me give the specified nick a burger.""" 31 | if message.group(2) is None: 32 | instance.reply('To whom should I give this cheeseburger?') 33 | else: 34 | instance.action(f'gives {message.group(2)} a freshly cooked cheeseburger.', message.sender) 35 | 36 | 37 | @example('.present MirahezeBot') 38 | @commands('present') 39 | def present(instance: bot, message: trigger) -> None: 40 | """Make me give the specified nick a present.""" 41 | if message.group(2) is None: 42 | instance.reply('To whom should I give this present?') 43 | else: 44 | instance.action(f'gives {message.group(2)} a present.', message.sender) 45 | 46 | 47 | @example('.hotchoc MirahezeBot') 48 | @commands('hotchoc', 'hotchocolate') 49 | def hotchoc(instance: bot, message: trigger) -> None: 50 | """Make me give the specified nick a hot chocolate.""" 51 | if message.group(2) is None: 52 | instance.reply('To whom should I give this hot chocolate?') 53 | else: 54 | instance.action( 55 | f'gives {message.group(2)} a warm, velvety salted caramel hot chocolate with cream and marhsmellows.', 56 | message.sender, 57 | ) 58 | -------------------------------------------------------------------------------- /MirahezeBots/tests/models.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | """SQL models for sopel database.""" 3 | import sys 4 | 5 | from sqlalchemy import Column, Integer, String, create_engine 6 | from sqlalchemy.ext.declarative import declarative_base 7 | 8 | Base = declarative_base() 9 | 10 | 11 | class NickNames(Base): 12 | """Model for 'nicknames' table.""" 13 | 14 | __tablename__ = 'nicknames' 15 | plugin = Column(String, primary_key=True) 16 | key = Column(String, primary_key=True) 17 | value = Column(String) 18 | 19 | def __str__(self): 20 | """Return main output.""" 21 | return f'{self.__tablename__} <{self.key}, {self.value}>' 22 | 23 | 24 | class NickValues(Base): 25 | """Model for 'nick_values' table.""" 26 | 27 | __tablename__ = 'nick_values' 28 | nick_id = Column(Integer, primary_key=True) 29 | key = Column(String, primary_key=True) 30 | value = Column(String) 31 | 32 | def __str__(self): 33 | """Return main output.""" 34 | return NickNames.__str__(self) 35 | 36 | 37 | class ChannelValues(Base): 38 | """Model for 'channel_values' table.""" 39 | 40 | __tablename__ = 'channel_values' 41 | channel = Column(String, primary_key=True) 42 | key = Column(String, primary_key=True) 43 | value = Column(String) 44 | 45 | def __str__(self): 46 | """Return main output.""" 47 | return NickNames.__str__(self) 48 | 49 | 50 | class PluginValues(Base): 51 | """Model for 'plugin_values' table.""" 52 | 53 | __tablename__ = 'plugin_values' 54 | plugin = Column(String, primary_key=True) 55 | key = Column(String, primary_key=True) 56 | value = Column(String) 57 | 58 | def __str__(self): 59 | """Return main output.""" 60 | return NickNames.__str__(self) 61 | 62 | 63 | class Welcome(Base): 64 | """Model for 'welcome' table.""" 65 | 66 | __tablename__ = 'welcome' 67 | welcome_id = Column(Integer, primary_key=True) 68 | nick_id = Column(Integer) 69 | account = Column(String) 70 | channel = Column(String) 71 | timestamp = Column(String) 72 | message = Column(String) 73 | 74 | def __str__(self): 75 | """Return main output.""" 76 | return Welcome.__str__(self) 77 | 78 | 79 | if __name__ == '__main__': 80 | try: 81 | engine = create_engine(f'sqlite:///{sys.argv[1]}', echo=True) 82 | except IndexError: 83 | engine = create_engine('sqlite:///example-model.db', echo=True) 84 | Base.metadata.create_all(engine) 85 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # ******** NOTE ******** 12 | 13 | name: "CodeQL" 14 | 15 | on: 16 | push: 17 | branches: 18 | - dev 19 | - release 20 | pull_request_target: 21 | schedule: 22 | - cron: '25 22 * * 6' 23 | 24 | jobs: 25 | analyze: 26 | name: Analyze 27 | runs-on: ubuntu-latest 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | language: [ 'python' ] 33 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 34 | # Learn more... 35 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 36 | 37 | steps: 38 | - name: Cancel Previous Runs 39 | uses: styfle/cancel-workflow-action@0.10.0 40 | with: 41 | all_but_latest: true 42 | access_token: ${{ github.token }} 43 | - name: Checkout repository 44 | uses: actions/checkout@v3 45 | - name: Setup python 3.9 46 | uses: actions/setup-python@v4 47 | with: 48 | python-version: 3.9 49 | # Initializes the CodeQL tools for scanning. 50 | - name: Initialize CodeQL 51 | uses: github/codeql-action/init@v2 52 | with: 53 | languages: ${{ matrix.language }} 54 | # If you wish to specify custom queries, you can do so here or in a config file. 55 | # By default, queries listed here will override any specified in a config file. 56 | # Prefix the list here with "+" to use these queries and those in the config file. 57 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 58 | 59 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 60 | # If this step fails, then you should remove it and run the build manually (see below) 61 | - name: Autobuild 62 | uses: github/codeql-action/autobuild@v2 63 | 64 | # ℹ️ Command-line programs to run using the OS shell. 65 | # 📚 https://git.io/JvXDl 66 | 67 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 68 | # and modify them (or add more) to build your code if your project 69 | # uses a compiled language 70 | 71 | #- run: | 72 | # make bootstrap 73 | # make release 74 | 75 | - name: Perform CodeQL Analysis 76 | uses: github/codeql-action/analyze@v2 77 | -------------------------------------------------------------------------------- /MirahezeBots/plugins/shortlinks.py: -------------------------------------------------------------------------------- 1 | """This plugin expands links to various websites.""" 2 | from sopel.plugin import commands, example 3 | from sopel import bot, trigger 4 | 5 | 6 | @commands('github', 'gh') 7 | @example('.github user') 8 | def ghuser(instance: bot, message: trigger) -> None: 9 | """Expand a link to github.""" 10 | try: 11 | instance.say(f'https://github.com/{message.group(2)}') 12 | except TypeError: 13 | instance.say('Syntax: .github user', message.sender) 14 | 15 | 16 | @commands('redditu') 17 | @example('.redditu example') 18 | def redditu(instance: bot, message: trigger) -> None: 19 | """Expand a link to reddit/u.""" 20 | try: 21 | instance.say(f'https://reddit.com/u/{message.group(2)}') 22 | except TypeError: 23 | instance.say('Syntax: .redditu example', message.sender) 24 | 25 | 26 | @commands('subred') 27 | @example('.subred example') 28 | def redditr(instance: bot, message: trigger) -> None: 29 | """Expand a link to reddit/r.""" 30 | try: 31 | instance.say(f'https://reddit.com/r/{message.group(2)}') 32 | except TypeError: 33 | instance.say('Syntax: .subred example', message.sender) 34 | 35 | 36 | @commands('wmca') 37 | @example('.wmca example') 38 | def wmca(instance: bot, message: trigger) -> None: 39 | """Expand a link to Wikimedia CentralAuth.""" 40 | try: 41 | instance.say( 42 | f'https://meta.wikimedia.org/wiki/Special:CentralAuth/{message.group(2).replace(" ", "_")}', 43 | ) 44 | except AttributeError: 45 | instance.say('Syntax: .wmca example', message.sender) 46 | 47 | 48 | @commands('mhca') 49 | @example('.mhca example') 50 | def mhca(instance: bot, message: trigger) -> None: 51 | """Expand a link to Miraheze Central Auth.""" 52 | try: 53 | instance.say( 54 | f'https://meta.miraheze.org/wiki/Special:CentralAuth/{message.group(2).replace(" ", "_")}', 55 | ) 56 | except AttributeError: 57 | instance.say('Syntax: .mhca example', message.sender) 58 | 59 | 60 | @commands('tw') 61 | @example('.tw user') 62 | def twlink(instance: bot, message: trigger) -> None: 63 | """Expand a link to Twitter.""" 64 | try: 65 | instance.say(f'https://twitter.com/{message.group(2)}') 66 | except TypeError: 67 | instance.say('Syntax: .tw user', message.sender) 68 | 69 | 70 | @commands('mh') 71 | @example('.mh wiki page') 72 | def mhwiki(instance: bot, message: trigger) -> None: 73 | """Expand a link to Miraheze wikis.""" 74 | try: 75 | options = message.group(2).split(' ', 1) 76 | if len(options) == 1: 77 | instance.say( 78 | f'https://meta.miraheze.org/wiki/{options[0].replace(" ", "_")}', 79 | ) 80 | elif len(options) == 2: 81 | wiki = options[0] 82 | page = options[1].replace(' ', '_') 83 | instance.say(f'https://{wiki}.miraheze.org/wiki/{page}') 84 | except AttributeError: 85 | instance.say('Syntax: .mh wiki page', message.sender) 86 | -------------------------------------------------------------------------------- /MirahezeBots/plugins/responses.py: -------------------------------------------------------------------------------- 1 | """responses.py - like a FAQ bot.""" 2 | 3 | from sopel import bot, trigger, config 4 | from sopel.config.types import StaticSection, ValidatedAttribute 5 | from sopel.plugin import commands, example, rate, require_account 6 | 7 | from MirahezeBots.version import SHORTVERSION, VERSION 8 | 9 | 10 | class ResponsesSection(StaticSection): 11 | """Create configuration for Sopel.""" 12 | 13 | support_channel = ValidatedAttribute('support_channel', str) 14 | 15 | 16 | def setup(instance: bot) -> None: 17 | """Set up the config section.""" 18 | instance.config.define_section('responses', ResponsesSection) 19 | 20 | 21 | def configure(configuration: config) -> None: 22 | """Set up the configuration options.""" 23 | configuration.define_section('responses', ResponsesSection, validate=False) 24 | configuration.responses.configure_setting('support_channel', 'Specify a support IRC channel (leave blank for none).') 25 | 26 | 27 | @commands('addchannel') 28 | @example('.addchannel (insert which)') 29 | @rate(user=120, channel=240, server=60) 30 | @require_account() 31 | def addchan(instance: bot, message: trigger) -> None: 32 | """Reply to channel request message.""" 33 | admins = ' '.join(map(str, instance.config.core.admin_accounts)) 34 | if instance.config.responses.support_channel is not None: 35 | instance.say( 36 | f'Hey {admins}, {message.nick} would like to have me in their channel: {message.group(2)}', 37 | instance.config.responses.support_channel, 38 | ) 39 | if message.sender != instance.config.responses.support_channel: 40 | instance.reply( 41 | f'Request sent! Action upon the request should be taken shortly. Thank you for using {instance.nick}!', 42 | ) 43 | 44 | 45 | @commands('gj', 'gw') 46 | @example('.gj (nick)') 47 | @rate(user=2, channel=1, server=0) 48 | def gj(instance: bot, message: trigger) -> None: 49 | """Tell the user that they are doing good work.""" 50 | instance.say(f"You're doing good work, {message.group(2)}!") 51 | 52 | 53 | @commands('cancelreminder') 54 | @example('.cancelreminder (insert reminder message here)') 55 | @rate(user=2, channel=1, server=0) 56 | def cancel(instance: bot, message: trigger) -> None: 57 | """Cancel reminder.""" 58 | admins = ' '.join(map(str, instance.config.core.admin_accounts)) 59 | instance.reply(f"Pinging {admins} to cancel {message.nicks}'s reminder.") 60 | 61 | 62 | @commands('botversion', 'bv') 63 | @example('.botversion') 64 | @rate(user=2, channel=1, server=0) 65 | def botversion(instance: bot, message: trigger) -> None: # noqa: U100 66 | """List the current version of the bot.""" 67 | instance.reply(f'The current version of this bot is {VERSION} ({SHORTVERSION})') 68 | 69 | 70 | @commands('source', 'botsource') 71 | @example('.source') 72 | @rate(user=2, channel=1, server=0) 73 | def githubsource(instance: bot, message: trigger) -> None: # noqa: U100 74 | """Give the link to MirahezeBot's Github.""" 75 | instance.reply('My code can be found here: https://github.com/MirahezeBots/MirahezeBots') 76 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: 'Main CI Build' 2 | on: [push, pull_request_target] 3 | 4 | jobs: 5 | test: 6 | name: Test Build (Python ${{ matrix.python }} on ${{ matrix.os }}) 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | python: ['3.8', '3.9', '3.9-dev', '3.10-dev'] 11 | os: ['ubuntu-latest', 'macos-latest'] 12 | steps: 13 | - name: Cancel Previous Runs 14 | uses: styfle/cancel-workflow-action@0.10.0 15 | with: 16 | access_token: ${{ github.token }} 17 | - uses: actions/checkout@v3 18 | with: 19 | submodules: true 20 | - name: Setup Python ${{ matrix.python }} on ${{ matrix.os }} 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ matrix.python }} 24 | - name: Install packages 25 | run: | 26 | python -m pip install --upgrade pip==21.3.1 wheel 27 | pip install -r dev-requirements.txt 28 | pyproject-build --wheel --outdir dist . 29 | find dist -name "*.whl" | xargs pip3 install 30 | - name: Show python version and show package 31 | run: | 32 | python --version 33 | sopel --version 34 | pip show MirahezeBot-Plugins 35 | - name: Run tests 36 | run: | 37 | flake8 . 38 | pytest --pyargs MirahezeBots.tests 39 | bandit MirahezeBots -r -x MirahezeBots/test 40 | mypy -p MirahezeBots --exclude=build --ignore-missing-imports --disallow-untyped-defs 41 | pip install -r compatibility.txt 42 | pip check 43 | - name: Run pip freeze and pipdeptree 44 | run: | 45 | pip freeze 46 | pipdeptree 47 | - name: Show outdated packages 48 | run: pip list --outdated 49 | deploy: 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Cancel Previous Runs 53 | uses: styfle/cancel-workflow-action@0.10.0 54 | with: 55 | access_token: ${{ github.token }} 56 | - uses: actions/checkout@v3 57 | - name: Set up Python 3.9 58 | uses: actions/setup-python@v4 59 | with: 60 | python-version: 3.9 61 | - name: Install pypa/build 62 | run: | 63 | python -m pip install --upgrade pip==21.1.0 64 | pip install -r dev-requirements.txt 65 | - name: Build a binary wheel 66 | run: pyproject-build --wheel --outdir dist . 67 | - name: Publish to PyPi 68 | if: github.event_name == 'push' && github.ref == 'refs/heads/release' 69 | uses: pypa/gh-action-pypi-publish@master 70 | with: 71 | password: ${{ secrets.pypi_token }} 72 | sonarcloud: 73 | name: SonarCloud 74 | runs-on: ubuntu-latest 75 | steps: 76 | - name: Cancel Previous Runs 77 | uses: styfle/cancel-workflow-action@0.10.0 78 | with: 79 | access_token: ${{ github.token }} 80 | - uses: actions/checkout@v3 81 | with: 82 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 83 | - name: SonarCloud Scan 84 | uses: SonarSource/sonarcloud-github-action@master 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 87 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 88 | -------------------------------------------------------------------------------- /MirahezeBots/utils/phabapi.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | """Phabricator API intercation utility.""" 3 | 4 | from json import JSONDecodeError 5 | from urllib.parse import urlparse 6 | 7 | from requests import Session 8 | from requests_cache import install_cache, uninstall_cache 9 | 10 | BOLD = '\x02' 11 | 12 | 13 | def gettaskinfo(host, apikey, task=1, session=Session()): 14 | """Get information on a specific task.""" 15 | data = { 16 | 'api.token': apikey, 17 | 'constraints[ids][0]': task, 18 | } 19 | response = session.post( 20 | url=f'{host}/maniphest.search', 21 | data=data) 22 | response = response.json() 23 | try: 24 | result = response.get('result').get('data')[0] 25 | except AttributeError: 26 | return 'An error occurred while parsing the result.' 27 | except IndexError: 28 | return None 29 | #install_cache('phab_user_cache', expire_after=2628002, allowable_methods=('POST')) # a month 30 | ownerPHID = result.get('fields').get('ownerPHID') 31 | authorPHID = result.get('fields').get('authorPHID') 32 | if ownerPHID is not None: 33 | params = { 34 | 'api.token': apikey, 35 | 'constraints[phids][0]': ownerPHID, 36 | } 37 | response2 = session.post( 38 | url=f'{host}/user.search', 39 | data=params) 40 | try: 41 | response2 = response2.json() 42 | except JSONDecodeError as e: 43 | raise ValueError(f'Encountered {e} on {response2.text}') 44 | owner = response2.get('result').get('data')[0].get('fields').get('username') 45 | elif ownerPHID is None: 46 | owner = None 47 | if ownerPHID == authorPHID: 48 | author = owner 49 | else: 50 | params2 = { 51 | 'api.token': apikey, 52 | 'constraints[phids][0]': authorPHID, 53 | } 54 | response3 = session.post( 55 | url=f'{host}/user.search', 56 | data=params2) 57 | #uninstall_cache() 58 | response3 = response3.json() 59 | author = response3.get('result').get('data')[0].get('fields').get('username') 60 | priority = result.get('fields').get('priority').get('name') 61 | status = result.get('fields').get('status').get('name') 62 | output = f"{'https://' + str(urlparse(host).netloc)}/T{str(result['id'])} - " 63 | output = '{0}{2}{1}{2}, '.format(output, str(result.get('fields').get('name')), BOLD) 64 | output = output + 'authored by {1}{0}{1}, '.format(author, BOLD) 65 | output = output + 'assigned to {1}{0}{1}, '.format(owner, BOLD) 66 | output = output + 'Priority: {1}{0}{1}, '.format(priority, BOLD) 67 | output = output + 'Status: {1}{0}{1}'.format(status, BOLD) 68 | return output # noqa: R504 69 | 70 | 71 | def dophabsearch(host, apikey, querykey, limit=True, session=Session()): 72 | """Perform a maniphest search.""" 73 | data = { 74 | 'api.token': apikey, 75 | 'queryKey': querykey, 76 | } 77 | response = session.post( 78 | url=f'{host}/maniphest.search', 79 | data=data) 80 | response = response.json() 81 | result = response.get('result') 82 | try: 83 | data = result.get('data') 84 | except AttributeError: 85 | return None 86 | x = 0 87 | searchphab = [] 88 | while x < len(data): 89 | currdata = data[x] 90 | if x > 5 and limit: 91 | return ['Limit exceeded. Please perform this search directly on phab.'] 92 | searchphab.append(gettaskinfo(host, apikey, task=currdata.get('id'), session=session)) 93 | x = x + 1 94 | return searchphab 95 | -------------------------------------------------------------------------------- /MirahezeBots/utils/mwapihandler.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | """MediaWiki API Handler.""" 3 | import requests 4 | 5 | CONNECTERRMSG = 'Unable to conect to wiki' 6 | 7 | 8 | def login(url, session, username, password): 9 | """Login to MediaWiki API using bot password system.""" 10 | PARAMS_0 = { 11 | 'action': 'query', 12 | 'meta': 'tokens', 13 | 'type': 'login', 14 | 'format': 'json', 15 | } 16 | try: 17 | request = session.get(url=url, params=PARAMS_0) 18 | DATA = request.json() 19 | except Exception: 20 | return ['Error', CONNECTERRMSG] 21 | 22 | LOGIN_TOKEN = DATA['query']['tokens']['logintoken'] 23 | 24 | PARAMS_1 = { 25 | 'action': 'login', 26 | 'lgname': username, 27 | 'lgpassword': password, 28 | 'lgtoken': LOGIN_TOKEN, 29 | 'format': 'json', 30 | } 31 | try: 32 | session.post(url, data=PARAMS_1) 33 | except Exception: 34 | return ['Error', CONNECTERRMSG] 35 | return ['Success', 'Logged in'] 36 | 37 | 38 | def gettoken(url, session, tokentype='csrftoken'): 39 | """Get a token from the meta::tokens api.""" 40 | PARAMS_2 = {'action': 'query', 'meta': 'tokens', 'format': 'json'} 41 | 42 | try: 43 | request = session.get(url=url, params=PARAMS_2) 44 | DATA = request.json() 45 | except Exception: 46 | return ['Error', CONNECTERRMSG] 47 | 48 | return DATA['query']['tokens'][tokentype] 49 | 50 | 51 | def makeaction(requestinfo, action, target, performer, reason, content=''): 52 | """Perform an action via the ACTIONS API.""" 53 | if action == 'edit': 54 | PARAMS = { 55 | 'action': 'edit', 56 | 'title': target, 57 | 'summary': reason + ' (' + performer + ')', 58 | 'appendtext': '\n* ' + performer + ': ' + reason, 59 | 'token': requestinfo[2], 60 | 'bot': 'true', 61 | 'format': 'json', 62 | } 63 | elif action == 'create': 64 | PARAMS = { 65 | 'action': 'edit', 66 | 'title': target, 67 | 'summary': reason, 68 | 'text': content, 69 | 'token': requestinfo[2], 70 | 'bot': 'true', 71 | 'format': 'json', 72 | 'contentmodel': 'wikitext', 73 | 'recreate': True, 74 | 'watchlist': 'nochange', 75 | } 76 | 77 | elif action == 'block': 78 | PARAMS = { 79 | 'action': 'block', 80 | 'user': target, 81 | 'expiry': 'infinite', 82 | 'reason': 'Blocked by ' + performer + ' for ' + reason, 83 | 'bot': 'false', 84 | 'token': requestinfo[2], 85 | 'format': 'json', 86 | } 87 | 88 | elif action == 'unblock': 89 | PARAMS = { 90 | 'action': 'unblock', 91 | 'user': target, 92 | 'reason': 'Requested by ' + performer + ' Reason: ' + reason, 93 | 'token': requestinfo[2], 94 | 'format': 'json', 95 | } 96 | 97 | elif action == 'delete': 98 | PARAMS = { 99 | 'action': 'delete', 100 | 'title': target, 101 | 'reason': 'Requested by ' + performer + ' Reason: ' + reason, 102 | 'token': requestinfo[2], 103 | 'format': 'json', 104 | } 105 | 106 | try: 107 | request = requestinfo[1].post(requestinfo[0], data=PARAMS) 108 | DATA = request.json() 109 | if DATA.get('error') is not None: 110 | return ['MWError', (DATA.get('error').get('info'))] 111 | return ['Success', f'{action} request sent. You may want to check the {action} log to be sure that it worked.'] 112 | except Exception: 113 | return [ 114 | 'Fatal', 115 | f'An unexpected error occurred. Did you type the wiki or user incorrectly? Do I have {action} on that wiki?', 116 | ] # noqa: JS102 117 | 118 | 119 | def main(performer, target, action, reason, url, authinfo, content=False, session=requests.Session()): 120 | """Execute a full API Sequence.""" 121 | lg = login(url, session, authinfo[0], authinfo[1]) 122 | if lg[0] == 'Error': 123 | return lg[1] 124 | TOKEN = gettoken(url, session, tokentype='csrftoken') 125 | if TOKEN[0] == 'Error': 126 | return TOKEN[1] 127 | if content: 128 | return makeaction([url, session, TOKEN], action, target, performer, reason, content)[1] 129 | return makeaction([url, session, TOKEN], action, target, performer, reason)[1] 130 | -------------------------------------------------------------------------------- /MirahezeBots/plugins/welcome.py: -------------------------------------------------------------------------------- 1 | """welcome.py - Plugin to welcome users upon joining the channel.""" 2 | 3 | import codecs 4 | import os 5 | import re 6 | from typing import List, Dict, Optional 7 | 8 | from sopel import bot, trigger 9 | from sopel.tools import Identifier 10 | from sopel.plugin import commands, event, example, require_admin, rule 11 | 12 | DEFAULT_CHANNEL = '#miraheze' 13 | USERNAME_RE = re.compile(r'[A-Za-z0-9\[\]\{\}\-_|`]+$') 14 | CHANNEL_RE = re.compile(r'#[A-Za-z0-9#\-]+$') 15 | 16 | 17 | def send_welcome(nick: Identifier, chan: Identifier) -> Optional[str]: 18 | """Find the message to be sent.""" 19 | if chan == '#miraheze' and nick[:4] != 'Not-': 20 | return f'Hello {nick}! If you have any questions, feel free to ask and someone should answer soon.' 21 | if chan == '#miraheze-cvt': 22 | return f'Welcome {nick}. If you need to report spam or abuse, please feel free to notify any of the voiced (+v) users, if it contains personal information you can pm them, or email us at cvt [at] miraheze.org' # noqa: E501 23 | return None 24 | 25 | 26 | def setup(instance: bot) -> None: 27 | """Do required setup for this module.""" 28 | instance.known_users_filename = os.path.join( 29 | instance.config.core.homedir, 30 | f'{instance.nick}-{instance.config.core.host}.known_users.db', 31 | ) 32 | instance.known_users_list = load_known_users_list(instance.known_users_filename) 33 | 34 | 35 | def load_known_users_list(filename: str) -> Dict[str, List[str]]: 36 | """Load list of known users from database file.""" 37 | known_users = {} # type: Dict[str, List[str]] 38 | if os.path.isfile(filename): 39 | f = codecs.open(filename, 'r', encoding='utf-8') 40 | for line in f: 41 | line = line.rstrip('\n') 42 | if '\t' in line: 43 | channel, username = line.split('\t') 44 | else: 45 | channel = DEFAULT_CHANNEL 46 | username = line 47 | 48 | if channel in known_users: 49 | known_users[channel].append(username) 50 | else: 51 | known_users[channel] = [username] 52 | return known_users 53 | 54 | 55 | def save_known_users_list(filename: str, known_users_list: Dict) -> None: 56 | """Save list of known users to database file.""" 57 | f = codecs.open(filename, 'w', encoding='utf-8') 58 | for channel in known_users_list: 59 | for user in known_users_list[channel]: 60 | f.write(f'{channel}\t{user}\n') 61 | f.close() 62 | 63 | 64 | @event('JOIN') 65 | @rule('.*') 66 | def welcome_user(instance: bot, message: trigger) -> None: 67 | """Welcome users upon joining the channel.""" 68 | if message.nick == instance.nick: 69 | return 70 | 71 | if message.sender not in instance.known_users_list: 72 | instance.known_users_list[message.sender] = [] 73 | if message.account == '*' and message.nick not in instance.known_users_list[message.sender]: 74 | instance.known_users_list[message.sender].append(message.nick) 75 | welcome = send_welcome(message.nick, message.sender) 76 | if welcome is not None: 77 | instance.say(welcome) 78 | else: 79 | if (message.account and message.nick) not in instance.known_users_list[message.sender]: 80 | instance.known_users_list[message.sender].append(message.account) 81 | welcome = send_welcome(message.nick, message.sender) 82 | if welcome is not None: 83 | instance.say(welcome) 84 | 85 | save_known_users_list(instance.known_users_filename, instance.known_users_list) 86 | 87 | 88 | @commands('add_known', 'adduser') 89 | @example('.add_known nick #example or .adduser nick #example') 90 | @require_admin(message='Only admins can modify the known users list', reply=True) 91 | def add_known_user(instance: bot, message: trigger) -> None: 92 | """Add user to known users list.""" 93 | username = message.group(3) 94 | if message.group(4): 95 | channel = message.group(4) 96 | elif message.sender[0] == '#': 97 | channel = message.sender 98 | else: 99 | channel = DEFAULT_CHANNEL 100 | 101 | if not USERNAME_RE.match(username): 102 | instance.reply(f'Invalid username: {username}') 103 | return 104 | 105 | if not CHANNEL_RE.match(channel): 106 | instance.reply(f'Invalid channel name: {channel}') 107 | return 108 | 109 | if channel not in instance.known_users_list: 110 | instance.known_users_list[channel] = [] 111 | 112 | if username in instance.known_users_list[channel]: 113 | instance.say(f'{username} is already added to known users list of channel {channel}') 114 | return 115 | 116 | instance.known_users_list[channel].append(username) 117 | save_known_users_list(instance.known_users_filename, instance.known_users_list) 118 | instance.say(f'Okay, {username} is now added to known users list of channel {channel}') 119 | -------------------------------------------------------------------------------- /MirahezeBots/plugins/status.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | """status.py - Mediawiki Status Page Updater.""" 3 | 4 | from MirahezeBots_jsonparser import jsonparser as jp 5 | from sopel.config.types import StaticSection, ValidatedAttribute 6 | from sopel.plugin import commands, example, require_admin 7 | from sopel.tools import SopelMemory 8 | 9 | from MirahezeBots.utils import mwapihandler as mwapi 10 | 11 | pages = '' 12 | 13 | 14 | class StatusSection(StaticSection): 15 | """Create configuration for Sopel.""" 16 | 17 | datafile = ValidatedAttribute('datafile', str) 18 | bot_username = ValidatedAttribute('bot_username', str) 19 | bot_password = ValidatedAttribute('bot_password', str) 20 | support_channel = ValidatedAttribute('support_channel', str) 21 | 22 | 23 | def setup(bot): 24 | """Set up the config section & memory.""" 25 | bot.config.define_section('status', StatusSection) 26 | bot.memory['status'] = SopelMemory() 27 | bot.memory['status']['jdcache'] = jp.createdict(bot.settings.status.datafile) 28 | 29 | 30 | def configure(config): 31 | """Set up the configuration options.""" 32 | config.define_section('status', StatusSection, validate=False) 33 | config.status.configure_setting( 34 | 'datafile', 35 | 'What is the status data file?', 36 | ) 37 | config.status.configure_setting( 38 | 'bot_username', 39 | 'What is the statusbot username? (from Special:BotPasswords)', 40 | ) 41 | config.status.configure_setting( 42 | 'bot_password', 43 | "What is the statusbot accounts's bot password? (from Special:BotPasswords)", 44 | ) 45 | config.status.configure_setting( 46 | 'support_channel', 47 | 'Specify a support IRC channel (leave blank for none).', 48 | ) 49 | 50 | 51 | def updatestatus(requestdata, authinfo, acldata, supportchan, session): 52 | """Update the /Status page of a user.""" 53 | if requestdata[2] in acldata['wikis']: 54 | wikiurl = str('https://' + acldata['wikis'][requestdata[2]]['url'] + '/w/api.php') 55 | sulgroup = acldata['wikis'][requestdata[2]]['sulgroup'] 56 | else: 57 | return 'Wiki could not be found' 58 | if requestdata[0] in acldata['users']: 59 | if sulgroup in acldata['users'][requestdata[0]]['groups']: 60 | request = [acldata['users'][requestdata[0]]['groups'][sulgroup], requestdata[3]] 61 | else: 62 | return f"Data not found for {sulgroup} in {requestdata[0]}, Keys were: {acldata['users'][requestdata[0]].keys()}" 63 | elif requestdata[1][0] in acldata['sulgroups'][sulgroup]['cloaks']: 64 | request = [requestdata[1][1], requestdata[3]] 65 | else: 66 | ERRNOAUTH = "You don't seem to be authorised to use this plugin. Check you are signed into NickServ and try again." 67 | if supportchan is None: 68 | return ERRNOAUTH 69 | return f'{ERRNOAUTH} If this persists, ask for help in {supportchan}.' 70 | return mwapi.main( 71 | performer=request[0], 72 | target=str('User:' + (str(request[0]) + '/Status')), 73 | action='create', 74 | reason=str('Updating status to ' + str(request[1]) + ' per ' + str(request[0])), 75 | url=wikiurl, 76 | authinfo=[authinfo[0], authinfo[1]], 77 | content=str(request[1]), 78 | session=session, 79 | ) 80 | 81 | 82 | @commands('status') 83 | @example('.status mhtest offline') 84 | def changestatus(bot, trigger): 85 | """Update the /Status subpage of Special:MyPage on the indicated wiki.""" 86 | options = [] 87 | try: 88 | options = trigger.group(2).split(' ') 89 | if len(options) == 2: 90 | wiki = options[0] 91 | status = options[1] 92 | host = trigger.host 93 | host = host.split('/') 94 | cont = 1 95 | elif len(options) > 2: 96 | wiki = options[0] 97 | host = trigger.host 98 | host = host.split('/') 99 | status = options[1] 100 | x = 2 101 | while x < len(options): 102 | status = status + ' ' + options[x] 103 | x = x + 1 104 | cont = 1 105 | else: 106 | bot.reply('Syntax: .status wikicode new-status') 107 | cont = 0 108 | except AttributeError as e: 109 | bot.reply('Syntax: .status wikicode new-status') 110 | bot.say(f'AttributeError: {e} from Status plugin in {trigger.sender}', bot.config.core.logging_channel) 111 | cont = 0 112 | if cont == 1: 113 | requestdata = [str(trigger.account), host, wiki, str(status)] 114 | response = updatestatus( 115 | requestdata, 116 | [bot.settings.status.bot_username, bot.settings.status.bot_password], 117 | bot.memory['status']['jdcache'], 118 | bot.settings.status.support_channel, 119 | bot.memory['shared']['session'], 120 | ) 121 | if response == 'create request sent. You may want to check the create log to be sure that it worked.': 122 | bot.reply('Success') 123 | else: 124 | bot.reply(str(response)) 125 | 126 | 127 | @require_admin(message='Only admins may purge cache.') 128 | @commands('resetstatuscache') 129 | def reset_status_cache(bot, trigger): # noqa: U100 130 | """Reset the cache of the channel management data file.""" 131 | bot.reply('Refreshing Cache...') 132 | bot.memory['status']['jdcache'] = jp.createdict(bot.settings.status.datafile) 133 | bot.reply('Cache refreshed') 134 | 135 | 136 | @require_admin(message='Only admins may check cache') 137 | @commands('checkstatuscache') 138 | def check_status_cache(bot, trigger): # noqa: U100 139 | """Validate the cache matches the copy on disk.""" 140 | result = jp.validatecache(bot.settings.status.datafile, bot.memory['status']['jdcache']) 141 | if result: 142 | bot.reply('Cache is correct.') 143 | else: 144 | bot.reply('Cache does not match on-disk copy') 145 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | **Changelog** 3 | 4 | Please see below for changes to MirahezeBot-Plugins 5 | # Version 9.1.4 6 | - Remove Python 3.6 support, various CI changes, bug fixes and compatibility changes 7 | # Version 9.1.1 8 | - Fixed developer requirements blocking newer versions of channelmgnt. 9 | # Version 9.1.0 10 | - CI: Migrate to GitHub-Actions 11 | - CI: Support Python 3.9, Windows and MacOS 12 | - dbclean: style changes 13 | - example-db: rename 14 | - adminlist: split to own package 15 | - joinall: split to own package 16 | - phab: use new jsonparser 17 | - pingpong: split to own package 18 | - responses: style changes and switch to VERSION keyword 19 | - rss: style changes 20 | - status: style changes & new jsonparser 21 | - welcome: style changes and remove get_filename, replace with bot.known_users_filename 22 | - jsonparser: split to own package 23 | - version: introduce VERSION, VERSIONARRAY and SHORTVERSION keywords 24 | - Security: Drop support for Sopel 7.0.5 & 7.0.6 25 | - requirements: update some and slacken others 26 | - build: style changes 27 | # Version 9.0.3 28 | - Fixed an issue affecting new installs due to a transient dependancy 29 | # Version 9.0.2 30 | ## Security fixes 31 | # Version 9.0.0 32 | ## Miscellaneous 33 | - travis: changed test configuration 34 | - responses: corrections and style changes 35 | - rss: style improvements 36 | - test models: style tweaks 37 | - test general: up'd max line length 38 | - test rss: style changes & replaced http:// with https:// 39 | ## Requirements 40 | - mwclient no longer required 41 | - Setuptools bumped from 49.5.0 to 49.6.0 42 | - flake8 is now required for developers 43 | - SQLAchemy is now at 1.3.19 44 | ## Plugins 45 | - all: removed future imports 46 | - channelmgnt: switched to caching the json config 47 | - channelmgnt: introduced a makemodechange system 48 | - channelmgnt: added support_channel config 49 | - status: replaced mwclient with a new util script, introduced cached json config, renamed other config 50 | - phab: Introduced channel specific configuration 51 | 52 | 53 | # Version 8.0.3 54 | ## Miscellaneous 55 | - Changes to the gitignore file & manifest to ensure proper handling of downloads & uploads 56 | - Changes to build configuration to prevent wasted checks 57 | - Cleaner Changelog 58 | ## requirements 59 | - Setuptools was bumped from 49.2.0 to 49.2.1 for developers 60 | ## phab 61 | - A bug was fixed with the task regex (T57) 62 | 63 | # Version 8.0.1 & 8.0.2 64 | - Changes to the build configuartion to prevent PyPi errors 65 | 66 | # Version 8.0.0 67 | In this update, we switch to using PyPi to install rather than copying to the plugins/modules folder. You should now delete our plugins from the plugins/modules folder and must switch to using PyPi to install. The minimum sopel version is now 7.0.5. Other requirements have changed. Please review compatibility with your install. 68 | ## goofy 69 | - This new fun module was added 70 | ## dbclean 71 | - This is now wrapped in a main() script and can be called using 'sopel-dbclean' 72 | ## mh_phab --> phab 73 | - Bug fixes 74 | - renamed from mh_phab to phab 75 | ## responses 76 | - bug fixes 77 | ## shortlinks 78 | - bug fixes 79 | ## Status 80 | - minor correction to help text 81 | 82 | # Version 7.2 83 | ## Status 84 | - Removed modules/config/*.csv 85 | ## Responses 86 | - Bug fixes 87 | ## channelmgnt 88 | - Switched to a new json config system 89 | # Version 7.1 90 | With Version 7.1, we bring you a fancy new name as MirahezeBot and some 91 | bug fixes and improvements. 92 | 93 | Please note that with this version we no longer support python 3.5, please upgrade to python 3.6 or above. 94 | 95 | ## Phabricator 96 | This module now supports all phabricator installs with conduit enabled. 97 | 98 | ## Responses 99 | A support_channel configuration variable was introduced. 100 | 101 | ## Status 102 | * Removed deceprated tuple 103 | * Introduced support_channel, wiki_username, wiki_password and data_path cnfiguration. 104 | * some functions now use bot.reply 105 | 106 | ## models 107 | This was incorrectly placed in the modules folder and has been moved to tests. 108 | 109 | # Version 7 110 | ## mh_phab 111 | This has been completely rewrote to be more efficent. 112 | We've introduced more config options as well. 113 | ## dbclean 114 | This is a new cli script to help clean up databases 115 | ## adminlist 116 | Now uses the owner/admin account config rather than nickname. 117 | ## channelmgnt 118 | You can now set modes, we've improved documentation and fixed a few bugs 119 | ## find_updates 120 | Has been replaced by the upstream version 121 | ## joinall (was join) 122 | We've removed the join control and replaced it with joinall that forces the bot to join all channels in your config file 123 | ## responses 124 | Has had some merged from other responses and no longer breaks with spaces 125 | ## status 126 | Now works with non cloaked users 127 | ## welcome 128 | Now also recognises accounts 129 | ## Requirements 130 | We changed the way we install things from pip. 131 | You only need to install requirments.txt but you might find pip-install.txt has some more fun modules on. 132 | 133 | # Version 6 134 | ## Channel Management 135 | * Added option to set channel operators individually for each channel 136 | * Now supports inviting users 137 | * Bug fixes 138 | ## Mediawiki Status 139 | * Created to allow users to set a status on mediawiki wikis. 140 | * Compatible user script and template developed by RhinosF1 141 | * See meta.miraheze.org/wiki/Template:UserStatus and https://meta.miraheze.org/wiki/User:RhinosF1/status.js 142 | ## Join 143 | * Bug Fixes 144 | ## Responses 145 | * Added new ones 146 | * Removed poorly used ones 147 | ## Short Links 148 | * Created to allow you to access your favourite sites in fewer clicks 149 | ## Urls 150 | * Bug fixes 151 | 152 | # Version 5 153 | ## New modules added 154 | * test_module 155 | * channelmgnt 156 | 157 | ## Modules changed 158 | * urls 159 | * miraheze 160 | * adminlist 161 | 162 | # Version 3 163 | ## New modules added 164 | * mh_phab 165 | * welcome 166 | 167 | ## Modules updated 168 | * converse 169 | * adminlist 170 | * reminders 171 | 172 | # Version 2 173 | * Added an admin list command (.adminlist) 174 | * Added .accesslevel command 175 | * Added .gethelp command (pings helpful users in channels) 176 | * Added a converse module 177 | * Added a new reminder system 178 | -------------------------------------------------------------------------------- /MirahezeBots/plugins/phab.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | """phab.by - Phabricator Task Information Plugin.""" 3 | 4 | from MirahezeBots_jsonparser import jsonparser as jp 5 | from sopel import bot, trigger, config 6 | from sopel.config.types import (BooleanAttribute, ListAttribute, StaticSection, 7 | ValidatedAttribute) 8 | from sopel.plugin import commands, example, interval, require_admin, rule 9 | from sopel.tools import SopelMemory, Identifier 10 | 11 | from MirahezeBots.utils import phabapi 12 | 13 | 14 | class PhabricatorSection(StaticSection): 15 | """Set up configuration for Sopel.""" 16 | 17 | querykey = ListAttribute('querykey', str) 18 | api_token = ListAttribute('api_token', str) 19 | highpri_notify = BooleanAttribute('highpri_notify') 20 | highpri_channel = ValidatedAttribute('highpri_channel', str) 21 | datafile = ValidatedAttribute('datafile', str) 22 | 23 | 24 | def setup(instance: bot) -> None: 25 | """Create the config section & memory.""" 26 | instance.config.define_section('phabricator', PhabricatorSection) 27 | instance.memory['phab'] = SopelMemory() 28 | instance.memory['phab']['jdcache'] = jp.createdict(instance.settings.phabricator.datafile) 29 | 30 | 31 | def configure(configuration: config) -> None: 32 | """Set up the configuration options.""" 33 | configuration.define_section('phabricator', PhabricatorSection, validate=False) 34 | configuration.phabricator.configure_setting( 35 | 'api_token', 36 | 'Please enter a Phabricator API token.', 37 | ) 38 | configuration.phabricator.configure_setting( 39 | 'highpri_notify', 40 | 'Would you like to enable automatic notification of high priority tasks? (true/false)', 41 | ) 42 | configuration.phabricator.configure_setting( 43 | 'highpri_channel', 44 | 'If you enabled high priority notifications, what channel would you like them sent to? ' 45 | '(notifications will be sent once every week.', 46 | ) 47 | configuration.phabricator.configure_setting( 48 | 'datafile', 49 | 'File to read from to get channel specific data from', 50 | ) 51 | configuration.phabricator.configure_setting( 52 | 'querykey', 53 | 'Please enter a Phabricator query key.', 54 | ) 55 | 56 | 57 | def get_host_and_api_or_query_key(channel: Identifier, cache: dict, keys: list) -> list: 58 | """Get hostname,apikey and querykey for instance.""" 59 | if channel in cache: 60 | host = cache[str(channel)]['host'] 61 | arraypos = int(cache[str(host)]['arraypos']) 62 | apikey = keys[0][int(arraypos)] 63 | querykey = keys[1][int(arraypos)] 64 | else: 65 | host = cache['default']['host'] 66 | arraypos = int(cache[str(host)]['arraypos']) 67 | apikey = keys[0][int(arraypos)] 68 | querykey = keys[1][int(arraypos)] 69 | return [host, apikey, querykey] 70 | 71 | 72 | @commands('task') 73 | @example('.task 1') 74 | def phabtask(instance: bot, message: trigger) -> None: 75 | """Get information on a phabricator task.""" 76 | try: 77 | if message.group(2).startswith('T'): 78 | task_id = message.group(2).split('T')[1] 79 | else: 80 | task_id = message.group(2) 81 | info = get_host_and_api_or_query_key( 82 | message.sender, 83 | instance.memory['phab']['jdcache'], 84 | [ 85 | instance.settings.phabricator.api_token, 86 | instance.settings.phabricator.querykey, 87 | ], 88 | ) 89 | instance.reply( 90 | phabapi.gettaskinfo( 91 | info[0], 92 | info[1], 93 | task=task_id, 94 | session=instance.memory['shared']['session']), 95 | ) 96 | except AttributeError: 97 | instance.say('Syntax: .task (task ID with or without T)', message.sender) 98 | 99 | 100 | @rule('T[1-9][0-9]*') 101 | def phabtask2(instance: bot, message: trigger) -> None: 102 | """Get a Miraheze phabricator link to a the task number you provide.""" 103 | task_id = str(message.match.group(0))[1:] 104 | info = get_host_and_api_or_query_key( 105 | message.sender, 106 | instance.memory['phab']['jdcache'], 107 | [ 108 | instance.settings.phabricator.api_token, 109 | instance.settings.phabricator.querykey, 110 | ], 111 | ) 112 | instance.reply( 113 | phabapi.gettaskinfo( 114 | info[0], 115 | info[1], 116 | task=task_id, 117 | session=instance.memory['shared']['session'], 118 | ), 119 | ) 120 | 121 | 122 | @interval(604800) # every week 123 | def high_priority_tasks_notification(instance: bot, message: trigger) -> None: # noqa: U100 124 | """Send regular update on high priority tasks.""" 125 | if instance.settings.phabricator.highpri_notify is True: 126 | info = get_host_and_api_or_query_key( 127 | instance.settings.phabricator.highpri_channel, 128 | instance.memory['phab']['jdcache'], 129 | [ 130 | instance.settings.phabricator.api_token, 131 | instance.settings.phabricator.querykey, 132 | ], 133 | ) 134 | result = phabapi.dophabsearch( 135 | info[0], 136 | info[1], 137 | info[2], 138 | session=instance.memory['shared']['session'], 139 | ) 140 | if result: 141 | instance.say('Your weekly high priority task update:', instance.settings.phabricator.highpri_channel) 142 | for task in result: 143 | instance.say(task, instance.settings.phabricator.highpri_channel) 144 | else: 145 | instance.say( 146 | 'High priority task update: Tasks exceeded limit or could not be found. Use ".highpri"', 147 | instance.settings.phabricator.highpri_channel, 148 | ) 149 | 150 | 151 | @commands('highpri') 152 | @example('.highpri') 153 | def forcehighpri(instance: bot, message: trigger) -> None: 154 | """Send full list of high priority tasks.""" 155 | info = get_host_and_api_or_query_key( 156 | message.sender, 157 | instance.memory['phab']['jdcache'], 158 | [ 159 | instance.settings.phabricator.api_token, 160 | instance.settings.phabricator.querykey, 161 | ], 162 | ) 163 | result = phabapi.dophabsearch( 164 | info[0], 165 | info[1], 166 | info[2], 167 | limit=False, 168 | session=instance.memory['shared']['session'], 169 | ) 170 | if result: 171 | for task in result: 172 | instance.reply(task) 173 | else: 174 | instance.reply('No tasks have high priority that I can see') 175 | 176 | 177 | @require_admin(message='Only admins may purge cache.') 178 | @commands('resetphabcache') 179 | def reset_phab_cache(instance: bot, message: trigger) -> None: # noqa: U100 180 | """Reset the cache of the channel management data file.""" 181 | instance.reply('Refreshing Cache...') 182 | instance.memory['phab']['jdcache'] = jp.createdict(instance.settings.phabricator.datafile) 183 | instance.reply('Cache refreshed') 184 | 185 | 186 | @require_admin(message='Only admins may check cache') 187 | @commands('checkphabcache') 188 | def check_phab_cache(instance: bot, message: trigger) -> None: # noqa: U100 189 | """Validate the cache matches the copy on disk.""" 190 | result = jp.validatecache(instance.settings.phabricator.datafile, instance.memory['phab']['jdcache']) 191 | if result: 192 | instance.reply('Cache is correct.') 193 | else: 194 | instance.reply('Cache does not match on-disk copy') 195 | --------------------------------------------------------------------------------