How to learn to draw the full picture of the pencil?
the Best material for the beginner artist's pencil. As it is easy to wash, can not be afraid to spoil the picture. All the bad contours and parts can be wipe with an eraser and work on. In addition, picture a simple pencil look not less advantageous than painting colors.
a Simple pencil - simple stuff?
to Draw beautiful isn't everything. To look good, you need to draw more and correctly. For this you just need to get a few lessons on drawing with a pencil. Do not rush to enroll in various art studios or expensive workshops. The basics of drawing, you will be able to study independently. Now the Internet has a huge number of free video lessons, intensives and tutorials in this subject. Everything else depends on practice and more practice.
After you have found a suitable drawing lessons pencil, do not expect a quick result. As with any training, you will need to put a lot of strength and perseverance. Do not ask too much, at least at first, and do not handle complex. Before you begin to paint the picture of a pencil, tamp hand on the most simple things such as lines and geometric shapes. It is from combinations of different shapes and consists of any object.
the Answer is simple - start with the purchase of materials for the job. Pencils come in several types. First, they are divided into artistic and office. Second, they have different markings, that is, different hard-soft. Hardness is indicated by the letter H on the pencil, and softness with the letter V. the Number before the letter indicates the degree of softness or hardness. You can purchase a set of art pencils, but the most convenient to start with would be the pencil marking HB. He is the most neutral of all, and working them, can not be afraid to permamnet pattern.
Right choice of paper also affects the quality of your drawings. It should be tight and not too loose. Normal office paper is better not to use it. It is very thin and smooth. Pencil is lubricated, and a desire to draw you after such paper can dramatically abyss.
Even such a simple thing as eraser, plays an important role. They, too, are professional, office and baby. Needless to say that your choice should be in favor of the professional.
once you will be confident to draw lines and shapes, you should learn the song to harmoniously arrange the objects on the sheet, hatching and shading to give the objects volume. To develop the concept of volume will draw three-dimensional geometric figures, preferably from life. Of course, a little one lying around the house plaster cubes and cylinders, so take any subject, any form. It can be any box or even a computer speaker, cans of spices or tea, Apple and so on.
Main thing when drawing - very carefully examine the object and try to depict it the way you see it. Special attention should be paid to pencil hatching. The bar runs jerky thin lines close to each other and in the same direction. They can be horizontal, vertical, diagonal. With their help it is possible to pass the volume, chiaroscuro, texture of the object.
If the beautiful touches will not happen, you can try the option of feathering. It is possible to achieve smooth black and white transitions, which looks very nice in the pictures in pencil. It is this: first, the drawing strokes are applied, and then a cotton swab or a soft piece of paper pounded. Fingers better not to grind, or may appear greasy stains. When this line is best to cross-apply, and feather to do from top to bottom.
When you master the technique of drawing on an acceptable level, you can choose the genre in which you will create paintings in pencil. Landscape, for example, a very attractive option and not as complicated in execution, as a portrait. Success in this depends more on imagination than strict adherence to the proportions. Although it requires basic knowledge about the prospect.
portraits, the situation is more complicated. I'll need to learn the basics of anatomical drawing and facial proportions. But do not be afraid of these words, it is not so difficult as it seems at first glance. The main thing is to learn the basic principle of the structure of the person and the practice. Man is the most complex object in the drawing, so don't give up after the first unsuccessful portraits. If you want to paint portraits from life, then do not start with a drawing of their family or friends. Start with the image itself. If you find it difficult to draw yourself in the mirror, take photos and work on photos. Following your models can become any photos of people on the Internet. Develop and improve your skill and eventually achieve good results.
do Not ignore the image of animals. In structure they are no less complex and interesting than the man himself and the animal world is huge and diverse. Here is a small example of how to draw animals in pencil.
If you have a pet, take a picture of it and use as a model. If not, you can take any photo on the Internet. Start with a General outline of the animal, denoting a simple form and posture.
After you make sure all the proportions are met, you can apply a light pencil stroke. Keep stroking, most intensely in the darkest of places. Your task as accurately as possible to portray the cat hair. Next, proceed to eyes carefully studied the peculiarities of the pupils. That hand won't accidentally smear the lower part, put it on a clean piece of paper. Don't forget about the glare in the eyes that give them liveliness.
Keep working, clearly denoting the bright and dark areas. You can lightly shade some areas, especially on the face, to make the transition from light to shadow is more gradual.
Finally I would like to mention some of the giants in this field, whose pictures are drawn in pencil, cause only one question: how do they do it?
Among them is Dirk Dzimirsky, German artist, painting highly detailed portraits in pencil.
Or, for example, Diego Fazio, an Italian, who, unlike Ziminskoe has no artistic education. However, his paintings pencil never tell from the photos.
There are a lot of artists, whose works you can learn something useful in terms of technique or story. This applies to both modern and old masters.


text:
The Bachelorette Finale 2019 Part 2 Recap: Who Did Hannah Pick and What Happened Next?
By Mike Krolak
Okay, first of all, how is Chris Harrison always right when he says "This will be the most dramatic finale ever"?!? How is it possible that The Bachelorette Season 15 and Bachelorette Hannah Brown were somehow able to deliver crazy moment after crazy moment after FOUR TIMES after crazy moment? This truly was a finale unlike any other in Bachelorette history, so standard finale questions such as "Who did Hannah pick?" or "Is Hannah engaged?" simply won't tell the whole story here. You're just gonna have to stream The Bachelorette Finale 2019, Part 2 here on ABC.com or in the ABC app, or continue reading this Bachelorette recap for this week's highlights!
It's proposal time! We've waited all season for this. I mean, sure, Hannah's been waiting for this moment for her entire life, but I still think we get some credit.
Hannah is ready. She's scared, but she's ready. She says that Tyler always puts her first, and she feels secure with him. She says that she and Jed are always honest with each other. The guys both pick out their rings.
But Hannah is conflicted. Very conflicted:
We haven't had a runaway lead since...oh, since last season of The Bachelor, like four months ago, when Colton jumped the fence. Still. That was pretty dramatic. But once she dusts herself off, it seems like Hannah knows what she wants, and that she just had a quick bout of nerves. She legit looks ready to do this.
She knows she's going to have to pretty thoroughly and substantially break one of their hearts today, but that's the price of everlasting love. So whose heart is going to be broken?
Tyler.
As always with this unflappable hunk of pure delight, he's nothing but sunshine and positivity stepping out of the van while chatting with Chris Harrison. But then:
Can't imagine there's ever been a more heartbreaking utterance of "Tyler..." in the history of civilization than when Hannah uses his name to cut him off. Tyler says it hurts but he'll always be her biggest fan. In case you were ever curious about what it looks like when you've been shot through the heart, here you go:
Oof.
The Main Event: A Proposal
Breakups are hard and that wasn't easy to watch, but we're here for a proposal so let's get right to it. By this point we know it's Jed, so we'll just toss you into the big moment:
Hannah's long journey is finally complete!
And that, friends, is what we call in this ol' business of show a "false ending." Because things are just starting to get juicy. Chris Harrison even said that in retrospect, this proposal could be called "cringeworthy."
It seems that on the second night of their engagement, Jed told Hannah that he had been hanging out with a girl a week before the show started. He broke it off, no biggie. Hannah is kind of irritated, but moves on. Then Hannah hears that there's a magazine article all about Jed's relationship, including the little quibble that he hadn't technically ended it before he came on the show. Hannah is questioning everything. She knows she has to talk to him face-to-face about all of this to be able to move forward.
The Real Main Event: Confronting Jed
Back in L.A., after all of this stuff goes down, Jed and Hannah meet up to discuss what's next. Hannah makes it clear that this is the time to come clean.
She asks him to tell the story of this girl. He says that they weren't exclusive, but they were sleeping together, going on cabin trips together, she met his parents, she threw him a surprise party, and her parents bought them a trip to the Bahamas. Hannah's not loving the story so far. Jed says he told the other woman that the show would be good for his music, and that before he left—and after he'd been drinking—he told her that he loved her. He said it didn't feel right, but it happened nonetheless. He says that to him, coming on the show was a goodbye to their relationship. Not verbally, but in his heart.
Honestly, you really just have to watch the full episode to get the full extent of Hannah's fierceness on this topic, because it's breathtaking. (Watch here!)
Hannah wonders what Jed was thinking while she was calling out Scott for having a girlfriend on Night One. Hannah was also irritated that Jed had told his friends that he "won" on The Bachelorette, and wonders if all of this was a game to him. She says some of this helps explain why his family didn't seem too excited about the prospect of Hannah and Jed getting engaged. Either way, she doesn't feel like any of this is what she said "yes" to that day in Greece:
The ring is off, y'all. The ring. Is off.
Hannah comes out and tells Chris that her life has been tough since then. She's cried, she's yelled, she's questioned her entire life. And then she breaks the news: She's not with Jed anymore. The engagement is fully off. She wanted honesty and trust, and that's not what she got. So that was that. And now Jed is here, and this will be the first time they've seen each other since the breakup. You know what that means...
The Okay-This-Time-for-Realsies Main Event: Confronting Jed (Again)
Jed walks out to silence from the crowd. He kicks it off with an apology: He regrets that his behavior and his actions ruined their relationship, because he truly fell in love with Hannah. He's sorry for his selfish behavior and the ripple effect it's had on friends, family, everyone. He says that when he was initially pressed for details about the other woman shortly after their engagement, he didn't tell Hannah the full story because he was acting out of fear instead of respect for Hannah—he didn't want to lose her. It seems heartfelt, and Hannah accepts his apology.
Jed says his feelings haven't changed: He's still in love with Hannah.
Unfortunately for Jed, her feelings have changed, and she's not in love with him like that any more. She says she's not trying to punish him, she just doesn't believe it's something that can be mended. He hates that it's not him, but he says he hopes she has a happy life. Furthermore, Hannah is proud of herself. If you ever need a pick-me-up, just listen to Hannah's fierce love for herself right here:
So, no engagement, sure...but renewed confidence and a strong sense of self? Not a bad deal, really.
Now, if you've been paying attention, there's one person we haven't talked to yet: Tyler.
And so...
The Really Realest Main Event? Maybe? WE DON'T EVEN KNOW ANYMORE
Chris asks Hannah how she feels about the man she sent home at the beginning of this episode, and she says her feelings for him haven't just gone away. Chris says they're gonna bring him out—and he's not sure what's going to happen.
Well the first thing that happens is every person in the joint screams for a good long while. It seems Tyler has a few fans in Bachelor Nation. For his part, Tyler is excited to see Hannah. He says he's enjoyed watching the season because he loves watching her just be a powerful, strong woman. He's sure she'll bounce back because "The world is hers, and I know that."
So back to those feelings Hannah kinda sorta maybe still had for Tyler. Is there...you know...a possible future in there? Watch:
Drinks then? After that wild finale, we sure need one.
And that's a wrap! Don't forget to watch the first week of Bachelor in Paradise MONDAY & TUESDAY 8|7c! Here's a preview of all the insane action you'll find on the beach:
If you read all that, we hereby ordain you as fully equipped to answer "What happened on The Bachelorette finale?" questions at work or school today. And if you just can't get enough Bachelorette, you can watch Hannah chat about the finale with Jimmy Kimmel here. See you next season!
The BacheloretteNews & BlogsSpoilers and RecapsThe Bachelorette Finale 2019 Part 2 Recap: Who Did Hannah Pick and What Happened Next?
Follow The Bachelorette:
ABC Updates
© 2023 ABC Entertainment

redpajama_set_name: RedPajamaCommonCrawl


text:
Q: Subset of a partially ordered set: Least upper bound and greatest lower bound. Let $(A, \leq)$ be a poset and $B \subseteq A$.
I need to show
i) B may have at most one least upper bound in A
ii) B may have at most one greatest lower bound in A.

A: This is really a matter of unpacking and applying the definitions of what it means

*

*for $A$ to be a poset (poset: "partially ordered set")

*

*(See also the section of that entry subtitled Extrema)

*for $B$ to be a subset of $A$,

*for a set to have a least upper bound (lub) and

*for a set to have greatest lower bound (glb).

Recall the definition of a least upper bound as:
Let S be a poset:

*

*$\alpha$ is an upper bound for S if $x \leq \alpha \space \forall x \in S$, (upper and lower bounds are NOT unique) and

*$\beta$ is the least upper bound for S if $\beta$ is an upper bound, and $\beta \leq \alpha$ whenever $\alpha$ is an upper bound for S.

A symmetrical definition defines the greatest lower bound.

redpajama_set_name: RedPajamaStackExchange


text:
How much does it cost to build a life coaching app?
Standard price for a life coaching app: $41,250
Find out the cost to build your own custom life coaching app:
What is a life coaching app?
A life coaching application is a web or mobile application with two sets of users: coaches and clients. Coaches use the app to view profiles of clients, schedule appointments, and send messages to clients. Clients use the app to view profiles of coaches, request appointments with specific coaches, and communicate with their assigned coach via an inbox. The app may also include a dashboard that enables administrators to analyze the performance of all coaches and clients.
Life coaching apps are similar to most other health and fitness apps, such as sleep tracking apps, calorie counter apps, diet apps, and exercise tracking apps. Headspace is one of the best-known life coaching apps.
Here are some recent life coaching app examples built with Crowdbotics:
An app that focuses on Mental Health/Spirituality, Physical Fitness and Financial Wellness.
A nonprofit mentoring or coaching app for everyday issues people have.
What is the typical cost to build a life coaching app?
A life coaching app usually costs $41,250 to build. However, the total cost can be as low as $5,000 or as high as $100,000. A life coaching app with a low number of features (also known as a "minimum viable product", or MVP) will be more affordable than an app that includes all intended functionality.
For example, here are some recent life coaching app price quotes from Crowdbotics:
How long does it take to build a life coaching app?
A life coaching app usually takes 550 hours to build. However, a life coaching app can be built in as few as 67 hours, or in as many as 1,333 hours. The exact timeline mostly depends on how complicated your app is. As a general rule, it will take longer if you require highly custom designs, niche features, or non-standard release platforms.
For example, the most recent life coaching apps built with Crowdbotics received the following hourly estimates:
How to successfully grow your life coaching application
A life coaching app can grow by making it easy for users to continue their coaching experience online at any time. Life coaching apps are built specifically for users who want to complete a full program in their own time, and these apps often provide additional content that can't all fit into one session. By keeping the user engaged over a longer period of time, the life coaching app provides a higher level of value and creates a higher-quality customer base for future business development.
Risks and challenges of building a life coaching application
A life coaching app faces two main risk categories: user safety and legal compliance. Since users are often sharing deeply personal information about their lives, it is important to provide users with mechanisms to block or report abusive users. The app should also be built to ensure that sensitive personal information is not publicly available. Additionally, the app should comply with all applicable laws related to protecting user privacy and security.


text:
Q: Laravel eager loading & whereHas Several questions have been asked about this but none provided a satisfactory explanation for this problem.
I run the following query:
$return = Vacation::whereHas('dates', function ($query) use ($from, $to) {
    $query->whereBetween('start_date', [$from, $to]);
})->get();

According to the query log, this produces the following SQL. The returned result is correct but there is a problem with the JOINed data.
select * from `vacations` where exists (select * from `vacation_dates` where `vacations`.`id` = `vacation_dates`.`vacation_id` and `start_date` between '2017-08-01 00:00:00' and '2017-08-30 00:00:00')

Since there's no JOIN, the related records are added afterwards through eager loading and the constraint is lost.
The scenario involves Vacations that have multiple start / stop dates and I want to check which Vacations have a start_date within the $start and $end date range.
If there's no occurrences, no records are returned.
When there is an occurrence, the Vacation is returned with ALL the dates and not just the ones in the constraint.
I understand how / what it's doing but don't see how I can get what I need: the record with the joined data that follows the constraint.
Anyone?

A: Solution:
$callback = function($query) use($from, $to) {
            $query->whereBetween('start_date', [$from, $to]);
        };
        $return = Vacation::whereHas('dates', $callback)->with(['dates' => $callback])->get();

Solution is from this SO post

A: Same answer as @stef gave, but better looking syntax (I believe)
$return = Vacation::query()
    ->whereHas('dates', $datesFilter = function($query) use ($from, $to) {
        $query->whereBetween('start_date', [$from, $to]);
    })
    ->with(['dates' => $datesFilter])
    ->get();


text:
Home ▸ What Is Brian Quinn's Net Worth And How Much Does He Make On Impractical Jokers?
What Is Brian Quinn's Net Worth And How Much Does He Make On Impractical Jokers?
Brian Quinn and his lifelong friends have created truTV and Comedy Central's highest-running and top-rated original series Impractical Jokers. Through his depictions in the hilariously outrageous hidden camera dares and punishments on the show, he has become a fan-favorite joker. He has been able to boost his finances through his earnings on the show and the proceeds from their live tours which have made a tremendous impact on Brian Quinn's net worth. He has also worked in the capacity of a writer and producer, having served as the executive producer of Impractical Jokers from 2011 to 2018 alongside his co-stars. In addition to being a part of the main cast of the reality game show, Quinn who is popularly known as 'Q' has also featured in several other shows and movies and has currently kick-started a brewery business in New York. To learn more about Brian Quinn's net worth and how much he makes on the hidden camera reality game show, scroll further.
How Much Does Brian Quinn Make On Impractical Jokers?
Prior to the debut of the game show in December 2011 on truTV, Brian Quinn along with his lifelong friends; Joseph Gatto, James Murray and Salvatore Vulcano has already gained prominence as The Tenderloins, a Comedy troupe. Their comedy sketches have gained millions of views on YouTube, Metacafe and other websites. Following their victory in the NBC, It's Your Show competition for the Sketch Time Thugs, where they took home a grand prize of $100,000 in 2007, the comedy troupe became immensely popular.
As a result, the first season of Impractical Jokers accumulated over 32 million views and has continued to gain online success with the subsequent seasons. Brian Quinn has been on the show since the first season was debuted in 2011 till present that the show has been renewed for the ninth season which is scheduled to premiere in 2020. Through his works on the show, he is reportedly earning $50,000 per episode, up from $26,000 he previously earned in 2015.
He has also embarked on tours with the Tenderloins as the troupe has toured steadily over the last four years. Their notably highly successful tours titled, Where's Larry?, Santiago Sent Us and Cranjis McBasketball Tours had in attendance hundreds of thousands of fans in the United States and the United Kingdom as well. They encountered three sold-out nights at Radio City Music Hall, a sold-out show at Madison Square Garden and five sold-out shows at London's O2 Arena.
Similarly, in February, Brian Quinn along with his comedy troupe co-stars had their 3rd annual Impractical Jokers Cruise for fans from Tampa to the Bahamas. Having accumulated millions of passionate fans, Quinn and his co-stars have translated the television success of Impractical Jokers into an international, multi-platform and experimental franchise including a brand new tour called The Cranjis McBasketball World Comedy Tour.
According to Pollstar's Top Comedy Tours, the Tenderloins' current tour has already grossed over $9.5 million with more 161,000 tickets sold over 36 shows making them one of the top-earning comedy acts in the US. In addition to Brian Quinn's net worth attaining a raise from World Tour grossing, it has also been inflated through his earnings from the occasional podcast titled, What Say You?, which he co-hosts with Salvatore Vulcano. The podcast which has gained much popularity was nominated for the Comedy, Entertainment and Best Produced Podcast Awards at the 10th Annual Podcast Award and was named Best New Show at the 2013 Stitcher Awards.
What is Brian Quinn's Net Worth?
Following the works of the jokers on the hidden camera reality game show, it has now become the No. 2 highest-rated comedy show on cable (behind HBO's Last Week Tonight) and No. 1 Unscripted Comedy Series on cable. With about nearly 5 million fans across social media platforms, Brian Quinn and his co-stars have a massive fan following which was reflected in the 1.2 billion social impressions they garnered in 2017.
See Also: Is Brian Quinn Married To A Wife Or Does He Have A Girlfriend?
With that, Brian Quinn and his co-stars have had many successful seasons of Impractical Jokers and have attained impressive net worths in the process. Hence, Brian Quinn's net worth has been estimated at $5 million according to Celebrity Net Worth. Aside from his works on the show, he has made appearances in other series and films such as Scroll Wheel of Time, Tacoma FD, Victor Crowley, Vulgar, Big Helium Dog, and has equally played voice roles in Jay and Silent Bob's Super Groovy Cartoon Movie, Tell 'Em Steve-Dave Makin' Clay and others.
Also, with the start of a new brewery business venture, Brian Quinn's net worth will certainly augment in the ensuing years. The famed comedian tapped into the brewery business on State Island by snapping up the name of a defunct brewery from New York City, Rubsam & Horrmann which he relaunched on 21st September 2019 at Flagship Brewery's taproom. The $25 tickets for the launching party that was graced by 1,300 people, sold out a week before the event. Currently, the iconic beer brand R&H already has a presence in several restaurants in Manhattan including the Ginger Man and Joe & Pat's.
Frank Blackfire Age, Wikipedia, Family, Height, Net Worth & Biography
Mark Beaufait Age, Wikipedia, Family, Height, Net Worth & Biography
Zoe LaVerne Height, Weight, Net Worth, Age, Wiki, Who, Instagram, Biography
Brittani Marcell Update – Where Is Her Killer Justin Hansen Today?
Brady Lewis Height, Weight, Net Worth, Age, Birthday, Wikipedia, Who, Instagram, Biography
Preity Zinta Age, Father, Instagram, Family, Last Movie, Net Worth, Education
Carolyn McRorie Age, Wikipedia, Family, Height, Net Worth & Biography
Joe Aska Age, Wikipedia, Family, Height, Net Worth & Biography
Norma Faye Kesey: Who is Larry McMurtry Wife?
Sergey Kabanovskiy Age, Wikipedia, Family, Height, Net Worth & Biography
Tatiana Kurbakova Age, Wikipedia, Family, Height, Net Worth & Biography
Dr. Alban Age, Wikipedia, Family, Height, Net Worth & Biography
Arthur Zanetti Age, Wikipedia, Family, Height, Net Worth & Biography
Mauricio Viana Age, Wikipedia, Family, Height, Net Worth & Biography
Benedykt Kocot Age, Wikipedia, Family, Height, Net Worth & Biography
Navana Akter Age, Wikipedia, Family, Height, Net Worth & Biography
Paul Sparer Bio, Wikipedia, Age, Height, Net Worth: Facts to Know About
Edith Belmont Age, Wikipedia, Family, Height, Net Worth & Biography
Mohamed Merza Age, Wikipedia, Family, Height, Net Worth & Biography
Chiara Vannucci Age, Wikipedia, Family, Height, Net Worth & Biography


text:
'use strict';
/** @typedef {GlobalRoom | GameRoom | ChatRoom} Room */
/**
 * Punishments
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * Handles the punishing of users on PS.
 *
 * There are four types of global punishments on PS. Locks, bans, namelocks and rangelocks.
 * This file contains the lists of users that have been punished (both IPs and usernames),
 * as well as the functions that handle the execution of said punishments.
 *
 * @license MIT license
 */

let Punishments = module.exports;

const FS = require('./lib/fs');

const PUNISHMENT_FILE = 'config/punishments.tsv';
const ROOM_PUNISHMENT_FILE = 'config/room-punishments.tsv';
const SHAREDIPS_FILE = 'config/sharedips.tsv';

const RANGELOCK_DURATION = 60 * 60 * 1000; // 1 hour
const LOCK_DURATION = 48 * 60 * 60 * 1000; // 48 hours
const GLOBALBAN_DURATION = 7 * 24 * 60 * 60 * 1000; // 1 week
const BATTLEBAN_DURATION = 48 * 60 * 60 * 1000; // 48 hours

const ROOMBAN_DURATION = 48 * 60 * 60 * 1000; // 48 hours
const BLACKLIST_DURATION = 365 * 24 * 60 * 60 * 1000; // 1 year

const USERID_REGEX = /^[a-z0-9]+$/;
const PUNISH_TRUSTED = false;

/**@type {{[k: string]: number}} */
const PUNISHMENT_POINT_VALUES = {MUTE: 2, BLACKLIST: 3, BATTLEBAN: 4, ROOMBAN: 4};
const AUTOLOCK_POINT_THRESHOLD = 8;

/**
 * a punishment is an array: [punishType, userid, expireTime, reason]
 * @typedef {[string, string, number, string]} Punishment
 */

/**
 * TODO: Properly Typescript this.
 * @typedef {User[]} PunishmentRow
 */

/**
 * @augments {Map<string, Punishment>}
 */
// @ts-ignore TODO: possible TypeScript bug
class PunishmentMap extends Map {
	get(/** @type {string} */ k) {
		const punishment = super.get(k);
		if (punishment) {
			if (Date.now() < punishment[2]) return punishment;
			this.delete(k);
		}
		return undefined;
	}
	has(/** @type {string} */ k) {
		return !!this.get(k);
	}
	/**
	 * @param {(punishment: Punishment, id: string, map: PunishmentMap) => void} callback
	 */
	forEach(callback) {
		super.forEach((punishment, k) => {
			if (Date.now() < punishment[2]) return callback(punishment, k, this);
			this.delete(k);
		});
	}
}

/**
 * ips is an ip:punishment Map
 */
Punishments.ips = new PunishmentMap();

/**
 * userids is a userid:punishment Map
 */
Punishments.userids = new PunishmentMap();

/**
 * @augments {Map<string, Map<string, Punishment>>}
 */
// @ts-ignore TODO: possible TypeScript bug
class NestedPunishmentMap extends Map {
	/**
	 * @param {string} k1
	 * @param {string} k2
	 * @param {Punishment} value
	 */
	nestedSet(k1, k2, value) {
		if (!this.get(k1)) {
			this.set(k1, new Map());
		}
		// @ts-ignore
		this.get(k1).set(k2, value);
	}
	/**
	 * @param {string} k1
	 * @param {string} k2
	 */
	nestedGet(k1, k2) {
		const subMap = this.get(k1);
		if (!subMap) return subMap;
		const punishment = subMap.get(k2);
		if (punishment) {
			if (Date.now() < punishment[2]) return punishment;
			this.nestedDelete(k1, k2);
		}
		return undefined;
	}
	/**
	 * @param {string} k1
	 * @param {string} k2
	 */
	nestedHas(k1, k2) {
		return !!this.nestedGet(k1, k2);
	}
	/**
	 * @param {string} k1
	 * @param {string} k2
	 */
	nestedDelete(k1, k2) {
		const subMap = this.get(k1);
		if (!subMap) return;
		subMap.delete(k2);
		if (!subMap.size) this.delete(k1);
	}
	/**
	 * @param {(punishment: Punishment, roomid: string, userid: string) => void} callback
	 */
	nestedForEach(callback) {
		this.forEach((subMap, k1) => {
			subMap.forEach((punishment, k2) => {
				if (Date.now() < punishment[2]) return callback(punishment, k1, k2);
				this.nestedDelete(k1, k2);
			});
		});
	}
}

/**
 * roomUserids is a roomid:userid:punishment nested Map
 */
Punishments.roomUserids = new NestedPunishmentMap();

/**
 * roomIps is a roomid:ip:punishment Map
 */
Punishments.roomIps = new NestedPunishmentMap();

/**
 * sharedIps is an ip:note Map
 */
Punishments.sharedIps = new Map();

/*********************************************************
 * Persistence
 *********************************************************/

// punishType is an allcaps string, for global punishments they can be anything in the punishmentTypes map.
// This map can be extended with custom punishments by chat plugins.
// Keys in the map correspond to punishTypes, values signify the way they should be displayed in /alt

/** @type {Map<string, string>} */
Punishments.punishmentTypes = new Map([
	['LOCK', 'locked'],
	['BAN', 'globally banned'],
	['NAMELOCK', 'namelocked'],
]);

// For room punishments, they can be anything in the roomPunishmentTypes map.
// This map can be extended with custom punishments by chat plugins.
// Keys in the map correspond to punishTypes, values signify the way they should be displayed in /alt
// By default, this includes:
//   'ROOMBAN'
//   'BLACKLIST'
//   'BATTLEBAN'
//   'MUTE' (used by getRoomPunishments)

/** @type {Map<string, string>} */
Punishments.roomPunishmentTypes = new Map([
	['ROOMBAN', 'banned'],
	['BLACKLIST', 'blacklisted'],
	['BATTLEBAN', 'battlebanned'],
	['MUTE', 'muted'],
]);

// punishments.tsv is in the format:
// punishType, userid, ips/usernames, expiration time
// room-punishments.tsv is in the format:
// punishType, roomid:userid, ips/usernames, expiration time

Punishments.loadPunishments = async function () {
	const data = await FS(PUNISHMENT_FILE).readIfExists();
	if (!data) return;
	for (const row of data.split("\n")) {
		if (!row || row === '\r') continue;
		const [punishType, id, altKeys, expireTimeStr, ...rest] = row.trim().split("\t");
		const expireTime = Number(expireTimeStr);
		if (punishType === "Punishment") continue;
		const keys = altKeys.split(',').concat(id);

		const punishment = [punishType, id, expireTime].concat(rest);
		if (Date.now() >= expireTime) {
			continue;
		}
		for (const key of keys) {
			if (!USERID_REGEX.test(key)) {
				Punishments.ips.set(key, punishment);
			} else {
				Punishments.userids.set(key, punishment);
			}
		}
	}
};

Punishments.loadRoomPunishments = async function () {
	const data = await FS(ROOM_PUNISHMENT_FILE).readIfExists();
	if (!data) return;
	for (const row of data.split("\n")) {
		if (!row || row === '\r') continue;
		const [punishType, id, altKeys, expireTimeStr, ...rest] = row.trim().split("\t");
		const expireTime = Number(expireTimeStr);
		if (punishType === "Punishment") continue;
		const [roomid, userid] = id.split(':');
		if (!userid) continue; // invalid format
		const keys = altKeys.split(',').concat(userid);

		const punishment = [punishType, userid, expireTime].concat(rest);
		if (Date.now() >= expireTime) {
			continue;
		}
		for (const key of keys) {
			if (!USERID_REGEX.test(key)) {
				Punishments.roomIps.nestedSet(roomid, key, punishment);
			} else {
				Punishments.roomUserids.nestedSet(roomid, key, punishment);
			}
		}
	}
};

Punishments.savePunishments = function () {
	FS(PUNISHMENT_FILE).writeUpdate(() => {
		const saveTable = new Map();
		Punishments.ips.forEach((/** @type {Punishment} */ punishment, /** @type {string} */ ip) => {
			const [punishType, id, ...rest] = punishment;
			if (id.charAt(0) === '#') return;
			let entry = saveTable.get(id);

			if (entry) {
				entry.keys.push(ip);
				return;
			}

			entry = {
				keys: [ip],
				punishType: punishType,
				rest: rest,
			};
			saveTable.set(id, entry);
		});
		Punishments.userids.forEach((/** @type {Punishment} */ punishment, /** @type {string} */ userid) => {
			const [punishType, id, ...rest] = punishment;
			if (id.charAt(0) === '#') return;
			let entry = saveTable.get(id);

			if (!entry) {
				entry = {
					keys: [],
					punishType: punishType,
					rest: rest,
				};
				saveTable.set(id, entry);
			}

			if (userid !== id) entry.keys.push(userid);
		});

		let buf = 'Punishment\tUser ID\tIPs and alts\tExpires\r\n';
		saveTable.forEach((entry, id) => {
			buf += Punishments.renderEntry(entry, id);
		});
		return buf;
	});
};

Punishments.saveRoomPunishments = function () {
	FS(ROOM_PUNISHMENT_FILE).writeUpdate(() => {
		const saveTable = new Map();
		Punishments.roomIps.nestedForEach((/** @type {Punishment} */ punishment, /** @type {string} */ roomid, /** @type {string} */ ip) => {
			const [punishType, punishUserid, ...rest] = punishment;
			const id = roomid + ':' + punishUserid;
			if (id.charAt(0) === '#') return;
			let entry = saveTable.get(id);

			if (entry) {
				entry.keys.push(ip);
				return;
			}

			entry = {
				keys: [ip],
				punishType: punishType,
				rest: rest,
			};
			saveTable.set(id, entry);
		});
		Punishments.roomUserids.nestedForEach((/** @type {Punishment} */ punishment, /** @type {string} */ roomid, /** @type {string} */ userid) => {
			const [punishType, punishUserid, ...rest] = punishment;
			const id = roomid + ':' + punishUserid;
			let entry = saveTable.get(id);

			if (!entry) {
				entry = {
					keys: [],
					punishType: punishType,
					rest: rest,
				};
				saveTable.set(id, entry);
			}

			if (userid !== punishUserid) entry.keys.push(userid);
		});

		let buf = 'Punishment\tRoom ID:User ID\tIPs and alts\tExpires\r\n';
		saveTable.forEach((entry, id) => {
			buf += Punishments.renderEntry(entry, id);
		});
		return buf;
	});
};

/**
 * @param {Object} entry
 * @param {string} id
 * @param {string} filename
 */
Punishments.appendPunishment = function (entry, id, filename) {
	if (id.charAt(0) === '#') return;
	let buf = Punishments.renderEntry(entry, id);
	FS(filename).append(buf);
};

/**
 * @param {Object} entry
 * @param {string} id
 * @return {string}
 */
Punishments.renderEntry = function (entry, id) {
	let row = [entry.punishType, id, entry.keys.join(',')].concat(entry.rest);
	return row.join('\t') + '\r\n';
};

Punishments.loadBanlist = async function () {
	const data = await FS('config/ipbans.txt').readIfExists();
	if (!data) return;
	let rangebans = [];
	for (const row of data.split("\n")) {
		const ip = row.split('#')[0].trim();
		if (!ip) continue;
		if (ip.includes('/')) {
			rangebans.push(ip);
		} else if (!Punishments.ips.has(ip)) {
			Punishments.ips.set(ip, ['BAN', '#ipban', Infinity]);
		}
	}
	Punishments.checkRangeBanned = Dnsbl.checker(rangebans);
};

// sharedips.tsv is in the format:
// IP, type (in this case always SHARED), note

Punishments.loadSharedIps = async function () {
	const data = await FS(SHAREDIPS_FILE).readIfExists();
	if (!data) return;
	for (const row of data.split("\n")) {
		if (!row || row === '\r') continue;
		const [ip, type, note] = row.trim().split("\t");
		if (!ip.includes('.')) continue;
		if (type !== 'SHARED') continue;

		Punishments.sharedIps.set(ip, note);
	}
};

/**
 * @param {string} ip
 * @param {string} note
 */
Punishments.appendSharedIp = function (ip, note) {
	let buf = `${ip}\tSHARED\t${note}\r\n`;
	FS(SHAREDIPS_FILE).append(buf);
};

Punishments.saveSharedIps = function () {
	let buf = 'IP\tType\tNote\r\n';
	Punishments.sharedIps.forEach((/** @type {string} */ note, /** @type {string} */ ip) => {
		buf += `${ip}\tSHARED\t${note}\r\n`;
	});

	FS(SHAREDIPS_FILE).write(buf);
};

setImmediate(() => {
	Punishments.loadPunishments();
	Punishments.loadRoomPunishments();
	Punishments.loadBanlist();
	Punishments.loadSharedIps();
});

/*********************************************************
 * Adding and removing
 *********************************************************/

/**
 * @param {User} user
 * @param {Punishment} punishment
 * @param {Set<string>?} recursionKeys
 * @return {PunishmentRow | undefined}
 */
Punishments.punish = function (user, punishment, recursionKeys) {
	let existingPunishment = Punishments.userids.get(toId(user.name));
	if (existingPunishment) {
		// don't reduce the duration of an existing punishment
		if (existingPunishment[2] > punishment[2]) {
			punishment[2] = existingPunishment[2];
		}

		// don't override stronger punishment types
		const types = ['LOCK', 'NAMELOCK', 'BAN'];
		if (types.indexOf(existingPunishment[0]) > types.indexOf(punishment[0])) {
			punishment[0] = existingPunishment[0];
		}
	}

	let keys = recursionKeys || new Set();
	let affected;

	if (!recursionKeys) {
		affected = user.getAltUsers(PUNISH_TRUSTED, true);
		for (let curUser of affected) {
			this.punish(curUser, punishment, keys);
		}
	}

	for (let ip in user.ips) {
		Punishments.ips.set(ip, punishment);
		keys.add(ip);
	}
	let lastUserId = user.getLastId();
	if (!lastUserId.startsWith('guest')) {
		Punishments.userids.set(lastUserId, punishment);
	}
	if (user.autoconfirmed) {
		Punishments.userids.set(user.autoconfirmed, punishment);
		keys.add(user.autoconfirmed);
	}
	if (user.trusted) {
		Punishments.userids.set(user.trusted, punishment);
		keys.add(user.trusted);
		if (!PUNISH_TRUSTED && affected) affected.unshift(user);
	}
	if (!recursionKeys) {
		const [punishType, id, ...rest] = punishment;
		keys.delete(id);
		Punishments.appendPunishment({
			keys: Array.from(keys),
			punishType: punishType,
			rest: rest,
		}, id, PUNISHMENT_FILE);
		return affected;
	}
};

/**
 * @param {string} userid
 * @param {Punishment} punishment
 */
Punishments.punishName = function (userid, punishment) {
	let foundKeys = Punishments.search(userid)[0].map((/** @type {string} */ key) => key.split(':')[0]);
	let userids = new Set([userid]);
	let ips = new Set();
	for (let key of foundKeys) {
		if (key.includes('.')) {
			ips.add(key);
		} else {
			userids.add(key);
		}
	}
	userids.forEach(id => {
		Punishments.userids.set(id, punishment);
	});
	ips.forEach(ip => {
		Punishments.ips.set(ip, punishment);
	});
	const [punishType, id, ...rest] = punishment;
	let affected = Users.findUsers(Array.from(userids), Array.from(ips), {includeTrusted: PUNISH_TRUSTED, forPunishment: true});
	userids.delete(id);
	Punishments.appendPunishment({
		keys: Array.from(userids).concat(Array.from(ips)),
		punishType: punishType,
		rest: rest,
	}, id, PUNISHMENT_FILE);

	return affected;
};

/**
 * @param {string} id
 * @param {string} punishType
 */
Punishments.unpunish = function (id, punishType) {
	id = toId(id);
	let punishment = Punishments.userids.get(id);
	if (punishment) {
		id = punishment[1];
	}
	// in theory we can stop here if punishment doesn't exist, but
	// in case of inconsistent state, we'll try anyway

	/** @type {false | string} */
	let success = false;
	Punishments.ips.forEach((/** @type {Punishment} */punishment, /** @type {string} */key) => {
		if (punishment[1] === id && punishment[0] === punishType) {
			Punishments.ips.delete(key);
			success = id;
		}
	});
	Punishments.userids.forEach((/** @type {Punishment} */punishment, /** @type {string} */key) => {
		if (punishment[1] === id && punishment[0] === punishType) {
			Punishments.userids.delete(key);
			success = id;
		}
	});
	if (success) {
		Punishments.savePunishments();
	}

	if (punishType === 'BAN' && success) Gold.evadeMonitor(null, null, {alts: success});

	return success;
};

/**
 * @param {Room} room
 * @param {User} user
 * @param {Punishment} punishment
 * @param {Set<string>?} recursionKeys
 * @return {PunishmentRow | undefined}
 */
Punishments.roomPunish = function (room, user, punishment, recursionKeys) {
	let roomid = typeof room === 'string' ? room : room.id;
	let keys = recursionKeys || new Set();
	/** @type {User[] | undefined} */
	let affected;

	if (!recursionKeys) {
		affected = user.getAltUsers(PUNISH_TRUSTED, true);
		for (let curUser of affected) {
			this.roomPunish(room, curUser, punishment, keys);
		}
	}

	for (let ip in user.ips) {
		Punishments.roomIps.nestedSet(roomid, ip, punishment);
		keys.add(ip);
	}
	if (!user.userid.startsWith('guest')) {
		Punishments.roomUserids.nestedSet(roomid, user.userid, punishment);
	}
	if (user.autoconfirmed) {
		Punishments.roomUserids.nestedSet(roomid, user.autoconfirmed, punishment);
		keys.add(user.autoconfirmed);
	}
	if (user.trusted) {
		Punishments.roomUserids.nestedSet(roomid, user.trusted, punishment);
		keys.add(user.trusted);
		if (!PUNISH_TRUSTED) {
			if (!affected) affected = [];
			affected.unshift(user);
		}
	}
	if (!recursionKeys) {
		const [punishType, id, ...rest] = punishment;
		keys.delete(id);
		Punishments.appendPunishment({
			keys: Array.from(keys),
			punishType: punishType,
			rest: rest,
		}, roomid + ':' + id, ROOM_PUNISHMENT_FILE);

		if (typeof roomid === 'string' || !(room.isPrivate === true || room.isPersonal || room.battle)) Punishments.monitorRoomPunishments(user);

		return affected;
	}
};

/**
 * @param {Room} room
 * @param {string} userid
 * @param {Punishment} punishment
 */
Punishments.roomPunishName = function (room, userid, punishment) {
	let foundKeys = Punishments.search(userid)[0].map((/** @type {string} */ key) => key.split(':')[0]);
	let userids = new Set([userid]);
	let ips = new Set();
	for (let key of foundKeys) {
		if (key.includes('.')) {
			ips.add(key);
		} else {
			userids.add(key);
		}
	}
	userids.forEach(id => {
		Punishments.roomUserids.nestedSet(room.id, id, punishment);
	});
	ips.forEach(ip => {
		Punishments.roomIps.nestedSet(room.id, ip, punishment);
	});
	const [punishType, id, ...rest] = punishment;
	let affected = Users.findUsers(Array.from(userids), Array.from(ips), {includeTrusted: PUNISH_TRUSTED, forPunishment: true});
	userids.delete(id);
	Punishments.appendPunishment({
		keys: Array.from(userids).concat(Array.from(ips)),
		punishType: punishType,
		rest: rest,
	}, room.id + ':' + id, ROOM_PUNISHMENT_FILE);

	if (!(room.isPrivate === true || room.isPersonal || room.battle)) Punishments.monitorRoomPunishments(userid);
	return affected;
};

/**
 * @param {Room} room
 * @param {string} id
 * @param {string} punishType
 * @param {boolean} ignoreWrite Flag to skip persistent storage.
 */
Punishments.roomUnpunish = function (room, id, punishType, ignoreWrite) {
	let roomid = typeof room === 'string' ? toId(room) : room.id;
	id = toId(id);
	let punishment = Punishments.roomUserids.nestedGet(roomid, id);
	if (punishment) {
		id = punishment[1];
	}
	// in theory we can stop here if punishment doesn't exist, but
	// in case of inconsistent state, we'll try anyway

	let success;
	const ipSubMap = Punishments.roomIps.get(roomid);
	if (ipSubMap) {
		ipSubMap.forEach((/** @type {Punishment} */punishment, /** @type {string} */key) => {
			if (punishment[1] === id && punishment[0] === punishType) {
				ipSubMap.delete(key);
				success = id;
			}
		});
	}
	const useridSubMap = Punishments.roomUserids.get(roomid);
	if (useridSubMap) {
		useridSubMap.forEach((/** @type {Punishment} */punishment, /** @type {string} */key) => {
			if (punishment[1] === id && punishment[0] === punishType) {
				useridSubMap.delete(key);
				success = id;
			}
		});
	}
	if (success && !ignoreWrite) {
		Punishments.saveRoomPunishments();
	}
	return success;
};

/*********************************************************
 * Specific punishments
 *********************************************************/

/**
 * @param {User} user
 * @param {number} expireTime
 * @param {string} id
 * @param {...string} reason
 * @return {PunishmentRow}
 */
Punishments.ban = function (user, expireTime, id, ...reason) {
	if (!id) id = user.getLastId();

	if (!expireTime) expireTime = Date.now() + GLOBALBAN_DURATION;
	let punishment = ['BAN', id, expireTime, ...reason];

	let affected = Punishments.punish(user, punishment);
	for (let curUser of affected) {
		curUser.locked = id;
		curUser.disconnectAll();
	}

	Gold.evadeMonitor(user, null, {type: 'globally banned', expires: expireTime});

	return affected;
};
/**
 * @param {string} name
 */
Punishments.unban = function (name) {
	return Punishments.unpunish(name, 'BAN');
};
/**
 * @param {User? | string} user
 * @param {number} expireTime
 * @param {string} id
 * @param {...string} reason
 * @return {PunishmentRow}
 */
Punishments.lock = function (user, expireTime, id, ...reason) {
	if (!user || typeof user === 'string') user = Users(id);
	if (!id && user) id = user.getLastId();

	if (!expireTime) expireTime = Date.now() + LOCK_DURATION;
	let punishment = ['LOCK', id, expireTime, ...reason];

	let affected = [];

	if (user) {
		affected = Punishments.punish(user, punishment);
	} else {
		affected = Punishments.punishName(id, punishment);
	}

	for (let curUser of affected) {
		curUser.locked = id;
		curUser.updateIdentity();
	}

	Gold.evadeMonitor(user, null, {type: 'locked', expires: expireTime});

	return affected;
};
/**
 * @param {User} user
 * @param {Room} room
 * @param {string} source
 * @param {string} reason
 * @param {string?} message
 * @param {boolean} week
 * @param {boolean} name
 */
Punishments.autolock = function (user, room, source, reason, message, week = false, name = false) {
	if (!message) message = reason;

	let punishment = `LOCKED`;
	let expires = null;
	if (week) {
		expires = Date.now() + 7 * 24 * 60 * 60 * 1000;
		punishment = `WEEKLOCKED`;
	}
	if (name) {
		punishment = `NAMELOCKED`;
		Punishments.namelock(user, expires, toId(user), `Autonamelock: ${user.name || toId(user)}: ${reason}`);
	} else {
		Punishments.lock(user, expires, toId(user), `Autolock: ${user.name || toId(user)}: ${reason}`);
	}
	Monitor.log(`[${source}] ${punishment}: ${message}`);
	const ipStr = typeof user !== 'string' ? ` [${user.latestIp}]` : '';
	Rooms.global.modlog(`(${toId(room)}) AUTO${name ? `NAME` : ''}LOCK: [${toId(user)}]${ipStr}: ${reason}`);
};
/**
 * @param {string} name
 */
Punishments.unlock = function (name) {
	let user = Users(name);
	let id = toId(name);
	/** @type {string[]} */
	let success = [];
	if (user && user.locked && !user.namelocked) {
		id = user.locked;
		user.locked = false;
		user.namelocked = false;
		user.updateIdentity();
		success.push(user.getLastName());
		if (id.charAt(0) !== '#') {
			Users.users.forEach(curUser => {
				if (curUser.locked === id) {
					curUser.locked = false;
					curUser.namelocked = false;
					curUser.updateIdentity();
					success.push(curUser.getLastName());
				}
			});
		}
	}
	if (Punishments.unpunish(name, 'LOCK')) {
		if (!success.length) success.push(name);
	}
	if (!success.length) return undefined;
	if (!success.some(v => toId(v) === id)) {
		success.push(id);
	}
	Gold.evadeMonitor(null, null, {alts: success});
	return success;
};
/**
 * @param {User} user
 * @param {number} expireTime
 * @param {string} id
 * @param {...string} reason
 * @return {PunishmentRow}
 */
Punishments.namelock = function (user, expireTime, id, ...reason) {
	if (!id) id = user.getLastId();

	if (!expireTime) expireTime = Date.now() + LOCK_DURATION;
	let punishment = ['NAMELOCK', id, expireTime, ...reason];

	let affected = Punishments.punish(user, punishment);
	for (let curUser of affected) {
		curUser.locked = id;
		curUser.namelocked = id;
		curUser.resetName();
		curUser.updateIdentity();
	}

	return affected;
};
/**
 * @param {string} name
 */
Punishments.unnamelock = function (name) {
	let user = Users(name);
	let id = toId(name);
	/** @type {string[]} */
	let success = [];
	let unpunished = Punishments.unpunish(name, 'NAMELOCK');
	if (user && user.locked) {
		id = user.locked;
		user.locked = false;
		user.namelocked = false;
		user.resetName();
		success.push(user.getLastName());
		if (id.charAt(0) !== '#') {
			Users.users.forEach(curUser => {
				if (curUser.locked === id) {
					curUser.locked = false;
					curUser.namelocked = false;
					curUser.resetName();
					success.push(curUser.getLastName());
				}
			});
		}
	}
	if (unpunished && !success.length) success.push(name);
	if (!success.length) return false;
	if (!success.some(v => toId(v) === id)) {
		success.push(id);
	}
	return success;
};
/**
 * @param {User} user
 * @param {number} expireTime
 * @param {string} id
 * @param {...string} reason
 * @return {PunishmentRow}
 */
Punishments.battleban = function (user, expireTime, id, ...reason) {
	if (!id) id = user.getLastId();

	if (!expireTime) expireTime = Date.now() + BATTLEBAN_DURATION;
	let punishment = ['BATTLEBAN', id, expireTime, ...reason];

	// Handle tournaments the user was in before being battle banned
	for (let games of user.games.keys()) {
		const gameRoom = Rooms(games).game;
		if (!gameRoom) continue; // this should never happen
		// @ts-ignore
		if (gameRoom.isTournament) {
			// @ts-ignore
			if (gameRoom.isTournamentStarted) {
				// @ts-ignore
				gameRoom.disqualifyUser(id, null, null);
				// @ts-ignore
			} else if (!gameRoom.isTournamentStarted) {
				// @ts-ignore
				gameRoom.removeUser(user);
			}
		}
	}

	return Punishments.roomPunish("battle", user, punishment);
};
/**
 * @param {string} userid
 */
Punishments.unbattleban = function (userid) {
	const user = Users(userid);
	if (user) {
		let punishment = Punishments.isBattleBanned(user);
		if (punishment) userid = punishment[1];
	}
	return Punishments.roomUnpunish("battle", userid, 'BATTLEBAN');
};
/**
 * @param {User} user
 * @return {Punishment | undefined}
 */
Punishments.isBattleBanned = function (user) {
	if (!user) throw new Error(`Trying to check if a non-existent user is battlebanned.`);

	let punishment = Punishments.roomUserids.nestedGet("battle", user.userid);
	if (punishment && punishment[0] === 'BATTLEBAN') return punishment;

	if (user.autoconfirmed) {
		punishment = Punishments.roomUserids.nestedGet("battle", user.autoconfirmed);
		if (punishment && punishment[0] === 'BATTLEBAN') return punishment;
	}

	for (let ip in user.ips) {
		punishment = Punishments.roomIps.nestedGet("battle", ip);
		if (punishment && punishment[0] === 'BATTLEBAN') {
			if (Punishments.sharedIps.has(ip) && user.autoconfirmed) return;
			return punishment;
		}
	}
};

/**
 * @param {string} range
 * @param {string} reason
 */
Punishments.lockRange = function (range, reason) {
	let punishment = ['LOCK', '#rangelock', Date.now() + RANGELOCK_DURATION, reason];
	Punishments.ips.set(range, punishment);
};
/**
 * @param {string} range
 * @param {string} reason
 */
Punishments.banRange = function (range, reason) {
	let punishment = ['BAN', '#rangelock', Date.now() + RANGELOCK_DURATION, reason];
	Punishments.ips.set(range, punishment);
};

/**
 * @param {Room} room
 * @param {User} user
 * @param {number} expireTime
 * @param {string} userId
 * @param {...string} reason
 * @return {PunishmentRow}
 */
Punishments.roomBan = function (room, user, expireTime, userId, ...reason) {
	if (!userId) userId = user.getLastId();

	if (!expireTime) expireTime = Date.now() + ROOMBAN_DURATION;
	let punishment = ['ROOMBAN', userId, expireTime].concat(reason);

	let affected = Punishments.roomPunish(room, user, punishment);
	for (let curUser of affected) {
		if (room.game && room.game.removeBannedUser) {
			room.game.removeBannedUser(curUser);
		}
		curUser.leaveRoom(room.id);
	}

	if (room.subRooms) {
		for (const subRoom of room.subRooms.values()) {
			for (const curUser of affected) {
				if (subRoom.game && subRoom.game.removeBannedUser) {
					subRoom.game.removeBannedUser(curUser);
				}
				curUser.leaveRoom(subRoom.id);
			}
		}
	}

	return affected;
};

/**
 * @param {Room} room
 * @param {User?} user
 * @param {number} expireTime
 * @param {string} userId
 * @param {...string} reason
 * @return {PunishmentRow}
 */
Punishments.roomBlacklist = function (room, user, expireTime, userId, ...reason) {
	if (!userId && user) userId = user.getLastId();
	if (!user) user = Users(userId);

	if (!expireTime) expireTime = Date.now() + BLACKLIST_DURATION;
	let punishment = ['BLACKLIST', userId, expireTime].concat(reason);

	let affected = [];

	if (!user || userId && userId !== user.userid) {
		affected = Punishments.roomPunishName(room, userId, punishment);
	}

	if (user) {
		affected = affected.concat(Punishments.roomPunish(room, user, punishment));
	}

	for (let curUser of affected) {
		if (room.game && room.game.removeBannedUser) {
			room.game.removeBannedUser(curUser);
		}
		curUser.leaveRoom(room.id);
	}

	if (room.subRooms) {
		for (const subRoom of room.subRooms.values()) {
			for (const curUser of affected) {
				if (subRoom.game && subRoom.game.removeBannedUser) {
					subRoom.game.removeBannedUser(curUser);
				}
				curUser.leaveRoom(subRoom.id);
			}
		}
	}

	return affected;
};

/**
 * @param {Room} room
 * @param {string} userid
 */
Punishments.roomUnban = function (room, userid) {
	const user = Users(userid);
	if (user) {
		let punishment = Punishments.isRoomBanned(user, room.id);
		if (punishment) userid = punishment[1];
	}
	return Punishments.roomUnpunish(room, userid, 'ROOMBAN');
};

/**
 * @param {Room} room
 * @param {string} userid
 * @param {boolean} ignoreWrite Flag to skip persistent storage.
 */
Punishments.roomUnblacklist = function (room, userid, ignoreWrite) {
	const user = Users(userid);
	if (user) {
		let punishment = Punishments.isRoomBanned(user, room.id);
		if (punishment) userid = punishment[1];
	}
	return Punishments.roomUnpunish(room, userid, 'BLACKLIST', ignoreWrite);
};

/**
 * @param {Room} room
 */
Punishments.roomUnblacklistAll = function (room) {
	const roombans = Punishments.roomUserids.get(room.id);
	if (!roombans) return false;

	/** @type {string[]} */
	let unblacklisted = [];

	roombans.forEach((/** @type {Punishment} */punishment, /** @type {string} */userid) => {
		if (punishment[0] === 'BLACKLIST') {
			Punishments.roomUnblacklist(room, userid, true);
			unblacklisted.push(userid);
		}
	});
	if (unblacklisted.length === 0) return false;
	Punishments.saveRoomPunishments();
	return unblacklisted;
};

/**
 * @param {string} ip
 * @param {string} note
 */
Punishments.addSharedIp = function (ip, note) {
	Punishments.sharedIps.set(ip, note);
	Punishments.appendSharedIp(ip, note);

	Users.users.forEach(user => {
		if (user.locked && user.locked !== user.userid && ip in user.ips) {
			if (!user.autoconfirmed) {
				user.semilocked = `#sharedip ${user.locked}`;
			}
			user.locked = false;

			user.updateIdentity();
		}
	});
};

/**
 * @param {string} ip
 */
Punishments.removeSharedIp = function (ip) {
	Punishments.sharedIps.delete(ip);
	Punishments.saveSharedIps();
};

/*********************************************************
 * Checking
 *********************************************************/

/**
 * @param {string} searchId
 * @return {[string[], (string | number)[]?]}
 */
Punishments.search = function (searchId) {
	/** @type {string[]} */
	let foundKeys = [];
	let foundRest = null;
	Punishments.ips.forEach((/** @type {Punishment} */ punishment, /** @type {string} */ ip) => {
		const [, id, ...rest] = punishment;

		if (searchId === id || searchId === ip) {
			foundKeys.push(ip);
			foundRest = rest;
		}
	});
	Punishments.userids.forEach((/** @type {Punishment} */punishment, /** @type {string} */userid) => {
		const [, id, ...rest] = punishment;

		if (searchId === id || searchId === userid) {
			foundKeys.push(userid);
			foundRest = rest;
		}
	});
	Punishments.roomIps.nestedForEach((/** @type {Punishment} */punishment, /** @type {string} */roomid, /** @type {string} */ip) => {
		const [, punishUserid, ...rest] = punishment;

		if (searchId === punishUserid || searchId === ip) {
			foundKeys.push(ip + ':' + roomid);
			foundRest = rest;
		}
	});
	Punishments.roomUserids.nestedForEach((/** @type {Punishment} */punishment, /** @type {string} */roomid, /** @type {string} */userid) => {
		const [, punishUserid, ...rest] = punishment;

		if (searchId === punishUserid || searchId === userid) {
			foundKeys.push(userid + ':' + roomid);
			foundRest = rest;
		}
	});

	return [foundKeys, foundRest];
};

/**
 * @param {string} name
 */
Punishments.getPunishType = function (name) {
	let punishment = Punishments.userids.get(toId(name));
	if (punishment) return punishment[0];
	let user = Users.get(name);
	if (!user) return;
	punishment = Punishments.ipSearch(user.latestIp);
	if (punishment) return punishment[0];
	return '';
};

/**
 * @param {Room} room
 * @param {string} name
 */
Punishments.getRoomPunishType = function (room, name) {
	let punishment = Punishments.roomUserids.nestedGet(room.id, toId(name));
	if (punishment) return punishment[0];
	let user = Users.get(name);
	if (!user) return;
	punishment = Punishments.roomIps.nestedGet(room.id, user.latestIp);
	if (punishment) return punishment[0];
	return '';
};

/**
 * Searches for IP in Punishments.ips
 *
 * For instance, if IP is '1.2.3.4', will return the value corresponding
 * to any of the keys in table match '1.2.3.4', '1.2.3.*', '1.2.*', or '1.*'
 *
 * @param {string} ip
 * @return {PunishmentRow | undefined}
 */
Punishments.ipSearch = function (ip) {
	let punishment = Punishments.ips.get(ip);
	if (punishment) return punishment;
	let dotIndex = ip.lastIndexOf('.');
	for (let i = 0; i < 4 && dotIndex > 0; i++) {
		ip = ip.substr(0, dotIndex);
		punishment = Punishments.ips.get(ip + '.*');
		if (punishment) return punishment;
		dotIndex = ip.lastIndexOf('.');
	}
	return undefined;
};

/**
 * @param {string} host
 * @return {string}
 */
Punishments.shortenHost = function (host) {
	if (host.slice(-7) === '-nohost') return host;
	let dotLoc = host.lastIndexOf('.');
	let tld = host.substr(dotLoc);
	if (tld === '.uk' || tld === '.au' || tld === '.br') dotLoc = host.lastIndexOf('.', dotLoc - 1);
	dotLoc = host.lastIndexOf('.', dotLoc - 1);
	return host.substr(dotLoc + 1);
};

// Defined in Punishments.loadBanlist
Punishments.checkRangeBanned = function () {};

/**
 * @param {User} user
 * @param {string} userid
 * @param {boolean} registered
 */
Punishments.checkName = function (user, userid, registered) {
	if (userid.startsWith('guest')) return;
	for (const roomid of user.inRooms) {
		Punishments.checkNewNameInRoom(user, userid, roomid);
	}
	let punishment = Punishments.userids.get(userid);
	let battleban = Punishments.isBattleBanned(user);
	if (!punishment && user.namelocked) {
		punishment = Punishments.userids.get(user.namelocked);
		if (!punishment) punishment = ['NAMELOCK', user.namelocked, 0];
	}
	if (!punishment && user.locked) {
		punishment = Punishments.userids.get(user.locked);
		if (!punishment) punishment = ['LOCK', user.locked, 0];
	}
	if (!battleban && Punishments.isBattleBanned(user)) {
		battleban = Punishments.roomUserids.get(Punishments.isBattleBanned(user));
		if (!battleban) battleban = ['BATTLEBAN', Punishments.isBattleBanned(user), 0];
	}

	const ticket = Chat.pages.help ? `<a href="view-help-request--appeal"><button class="button"><strong>Appeal your punishment</strong></button></a>` : '';

	if (battleban) {
		if (battleban[1] !== user.userid && Punishments.sharedIps.has(user.latestIp) && user.autoconfirmed) {
			Punishments.roomUnpunish("battle", userid, 'BATTLEBAN');
		} else {
			Punishments.roomPunish("battle", user, battleban);
			user.cancelReady();
			if (!punishment) {
				let appealLink = ticket || (Config.appealurl ? `appeal at: ${Config.appealurl}` : ``);
				// Prioritize popups for other global punishments
				user.send(`|popup||html|You are banned from battling${battleban[1] !== userid ? ` because you have the same IP as banned user: ${battleban[1]}` : ''}. Your battle ban will expire in a few days.${battleban[3] ? Chat.html `\n\nReason: ${battleban[3]}` : ``}${appealLink ? `\n\nOr you can ${appealLink}.` : ``}`);
				user.punishmentNotified = true;
				return;
			}
		}
	}
	if (!punishment) return;

	let id = punishment[0];
	let punishUserid = punishment[1];
	let reason = ``;
	if (punishment[3]) reason = `\n\nReason: ${Chat.escapeHTML(punishment[3])}`;
	let appeal = ``;
	if (user.permalocked && Config.appealurl) {
		appeal += `\n\nPermanent punishments can be appealed: <a href="${Config.appealurl}">${Config.appealurl}</a>`;
	} else if (ticket) {
		appeal += `\n\nIf you feel you were unfairly punished or wish to otherwise appeal, you can ${ticket}.`;
	} else if (Config.appealurl) {
		appeal += `\n\nIf you wish to appeal your punishment, please use: <a href="${Config.appealurl}">${Config.appealurl}</a>`;
	}
	let bannedUnder = ``;
	if (punishUserid !== userid) bannedUnder = ` because you have the same IP as banned user: ${punishUserid}`;

	if ((id === 'LOCK' || id === 'NAMELOCK') && punishUserid !== user.userid && Punishments.sharedIps.has(user.latestIp)) {
		if (!user.autoconfirmed) {
			user.semilocked = `#sharedip ${user.locked}`;
		}
		user.locked = false;

		user.updateIdentity();
		return;
	}
	if (registered && id === 'BAN') {
		user.popup(`Your username (${user.name}) is banned${bannedUnder}. Your ban will expire in a few days.${reason}${Config.appealurl ? `||||Or you can appeal at: ${Config.appealurl}` : ``}`);
		user.punishmentNotified = true;
		Punishments.punish(user, punishment);
		user.disconnectAll();
		return;
	}
	if (id === 'NAMELOCK' || user.namelocked) {
		user.send(`|popup||html|You are namelocked and can't have a username${bannedUnder}. Your namelock will expire in a few days.${reason}${appeal}`);
		user.locked = punishUserid;
		user.namelocked = punishUserid;
		user.resetName();
		user.updateIdentity();
	} else {
		if (punishUserid === '#hostfilter') {
			user.popup(`Due to spam, you can't chat using a proxy. (Your IP ${user.latestIp} appears to be a proxy.)`);
		} else if (!user.lockNotified) {
			user.send(`|popup||html|You are locked${bannedUnder}. ${user.permalocked ? `This lock is permanent.` : `Your lock will expire in a few days.`}${reason}${appeal}`);
		}
		user.lockNotified = true;
		if (user.userid === punishUserid) Punishments.punish(user, punishment);
		user.locked = punishUserid;
		user.updateIdentity();
	}
};

/**
 * @param {User} user
 * @param {Connection} connection
 */
Punishments.checkIp = function (user, connection) {
	let ip = connection.ip;
	let punishment = Punishments.ipSearch(ip);
	if (punishment) {
		if (Punishments.sharedIps.has(user.latestIp)) {
			if (!user.locked && !user.autoconfirmed) {
				user.semilocked = `#sharedip ${punishment[1]}`;
			}
		} else {
			user.locked = punishment[1];
			if (punishment[0] === 'NAMELOCK') {
				user.namelocked = punishment[1];
			}
		}
	}

	Dnsbl.reverse(ip).catch((/** @type {Error} */ e) => {
		// If connection.user is reassigned before async tasks can run, user
		// may no longer be equal to it.
		user = connection.user || user;
		// @ts-ignore
		if (e.code === 'EINVAL') {
			if (!user.locked && !user.autoconfirmed) {
				user.semilocked = '#dnsbl';
			}
			return null;
		}
		throw e;
	}).then((/** @type {string} */ host) => {
		user = connection.user || user;
		if (host) user.latestHost = host;
		Chat.hostfilter(host, user, connection);
	});

	if (Config.dnsbl) {
		Dnsbl.query(connection.ip).then((/** @type {boolean} */ isBlocked) => {
			user = connection.user || user;
			if (isBlocked) {
				if (!user.locked && !user.autoconfirmed) {
					user.semilocked = '#dnsbl';
				}
			}
		});
	}
};

/**
 * Connection flood table. Separate table from IP bans.
 * @type {Set<string>}
 */
let cfloods = Punishments.cfloods = new Set();

/**
 * IP bans need to be checked separately since we don't even want to
 * make a User object if an IP is banned.
 *
 * @param {Connection} connection
 * @return {string? | false}
 */
Punishments.checkIpBanned = function (connection) {
	let ip = connection.ip;
	if (cfloods.has(ip) || (Monitor.countConnection(ip) && cfloods.add(ip))) {
		connection.send(`|popup||modal|PS is under heavy load and cannot accommodate your connection right now.`);
		return '#cflood';
	}

	if (Punishments.sharedIps.has(ip)) return false;

	/** @type {false | string} */
	let banned = false;
	let punishment = Punishments.ipSearch(ip);
	if (punishment && punishment[0] === 'BAN') {
		banned = punishment[1];
	} else if (Punishments.checkRangeBanned(ip)) {
		banned = '#ipban';
	}
	if (!banned) return false;

	if (banned === '#ipban') {
		connection.send(`|popup||modal|Your IP (${ip}) is not allowed to connect to PS, because it has been used to spam, hack, or otherwise attack our server.||Make sure you are not using any proxies to connect to PS.`);
	} else {
		let appeal = (Config.appealurl ? `||||Or you can appeal at: ${Config.appealurl}` : ``);
		connection.send(`|popup||modal|You are banned because you have the same IP (${ip}) as banned user '${banned}'. Your ban will expire in a few days.${appeal}`);
	}
	Monitor.notice(`CONNECT BLOCKED - IP BANNED: ${ip} (${banned})`);

	return banned;
};

/**
 * @param {User} user
 * @param {string} roomid
 * @return {boolean | undefined}
 */
Punishments.checkNameInRoom = function (user, roomid) {
	let punishment = Punishments.roomUserids.nestedGet(roomid, user.userid);
	if (!punishment && user.autoconfirmed) {
		punishment = Punishments.roomUserids.nestedGet(roomid, user.autoconfirmed);
	}
	if (!punishment && Rooms(roomid).parent) punishment = Punishments.checkNameInRoom(user, Rooms(roomid).parent);
	if (!punishment) return false;
	if (punishment[0] === 'ROOMBAN' || punishment[0] === 'BLACKLIST') {
		return true;
	}
};

/**
 * @param {User} user
 * @param {string} userid The name into which the user is renamed.
 * @param {string} roomid
 */
Punishments.checkNewNameInRoom = function (user, userid, roomid) {
	let punishment = Punishments.roomUserids.nestedGet(roomid, userid);
	if (!punishment && Rooms(roomid).parent) punishment = Punishments.checkNewNameInRoom(user, userid, Rooms(roomid).parent);
	if (punishment) {
		if (punishment[0] !== 'ROOMBAN' && punishment[0] !== 'BLACKLIST') return;
		const room = Rooms(roomid);
		if (room.game && room.game.removeBannedUser) {
			room.game.removeBannedUser(user);
		}
		user.leaveRoom(room.id);
		return punishment;
	}
};

/**
 * @param {string? | boolean} userid
 * @return {string} Descriptive text for the remaining time until the punishment expires, if any.
 */
Punishments.checkLockExpiration = function (userid) {
	if (typeof userid === "boolean") return ``;
	const punishment = Punishments.userids.get(userid);

	if (punishment) {
		let user = Users(userid);
		if (user && user.permalocked) return ` (never expires; you are permalocked)`;
		let expiresIn = new Date(punishment[2]).getTime() - Date.now();
		let expiresDays = Math.round(expiresIn / 1000 / 60 / 60 / 24);
		let expiresText = '';
		if (expiresDays >= 1) {
			expiresText = `in around ${Chat.count(expiresDays, "days")}`;
		} else {
			expiresText = `soon`;
		}
		if (expiresIn > 1) return ` (expires ${expiresText})`;
	}

	return ``;
};

/**
 * @param {User} user
 * @param {string} roomid
 * @return {Punishment | undefined}
 */
Punishments.isRoomBanned = function (user, roomid) {
	if (!user) throw new Error(`Trying to check if a non-existent user is room banned.`);

	let punishment = Punishments.roomUserids.nestedGet(roomid, user.userid);
	if (punishment && (punishment[0] === 'ROOMBAN' || punishment[0] === 'BLACKLIST')) return punishment;

	if (user.autoconfirmed) {
		punishment = Punishments.roomUserids.nestedGet(roomid, user.autoconfirmed);
		if (punishment && (punishment[0] === 'ROOMBAN' || punishment[0] === 'BLACKLIST')) return punishment;
	}

	for (let ip in user.ips) {
		punishment = Punishments.roomIps.nestedGet(roomid, ip);
		if (punishment) {
			if (punishment[0] === 'ROOMBAN') {
				return punishment;
			} else if (punishment[0] === 'BLACKLIST') {
				if (Punishments.sharedIps.has(ip) && user.autoconfirmed) return;

				return punishment;
			}
		}
	}

	const room = Rooms(roomid);
	if (!room) throw new Error(`Trying to ban a user from a nonexistent room: ${roomid}`);

	if (room.parent) return Punishments.isRoomBanned(user, room.parent.id);
};

/**
 * Returns an array of all room punishments associated with a user.
 *
 * options.publicOnly will make this only return public room punishments.
 * options.checkIps will also check the IP of the user for IP-based punishments.
 *
 * @param {User | string} user
 * @param {Object?} options
 * @return {Array}
 */
Punishments.getRoomPunishments = function (user, options) {
	if (!user) return [];
	let userid = toId(user);
	let checkMutes = typeof user !== 'string';

	let punishments = [];

	for (const curRoom of Rooms.global.chatRooms) {
		if (!curRoom || curRoom.isPrivate === true || ((options && options.publicOnly) && (curRoom.isPersonal || curRoom.battle))) continue;
		let punishment = Punishments.roomUserids.nestedGet(curRoom.id, userid);
		if (punishment) {
			punishments.push([curRoom, punishment]);
			continue;
		} else if (options && options.checkIps) {
			// @ts-ignore
			for (let ip in user.ips) {
				punishment = Punishments.roomIps.nestedGet(curRoom.id, ip);
				if (punishment) {
					punishments.push([curRoom, punishment]);
					continue;
				}
			}
		}
		if (checkMutes && curRoom.muteQueue) {
			for (const entry of curRoom.muteQueue) {
				if (userid === entry.userid ||
					// @ts-ignore
					user.guestNum === entry.guestNum ||
					// @ts-ignore
					(user.autoconfirmed && user.autoconfirmed === entry.autoconfirmed)) {
					punishments.push([curRoom, ['MUTE', entry.userid, entry.time]]);
				}
			}
		}
	}

	return punishments;
};

/**
 * Notifies staff if a user has three or more room punishments.
 *
 * @param {User | string} user
 */
Punishments.monitorRoomPunishments = function (user) {
	// @ts-ignore
	if (user.locked) return;
	const userid = toId(user);

	const minPunishments = (typeof Config.monitorminpunishments === 'number' ? Config.monitorminpunishments : 3); // Default to 3 if the Config option is not defined or valid
	if (!minPunishments) return;

	let punishments = Punishments.getRoomPunishments(user, {publicOnly: true});

	if (punishments.length >= minPunishments) {
		let points = 0;

		let punishmentText = punishments.map((/** @type {[string, Punishment]} */ [room, punishment]) => {
			const [punishType, punishUserid, , reason] = punishment;
			if (punishType in PUNISHMENT_POINT_VALUES) points += PUNISHMENT_POINT_VALUES[punishType];
			let punishDesc = Punishments.roomPunishmentTypes.get(punishType);
			if (!punishDesc) punishDesc = `punished`;
			if (punishUserid !== userid) punishDesc += ` as ${punishUserid}`;

			if (reason) punishDesc += `: ${reason}`;
			return `<<${room}>> (${punishDesc})`;
		}).join(', ');

		if (Config.punishmentautolock && points >= AUTOLOCK_POINT_THRESHOLD) {
			let rooms = punishments.map((/** @type {[string]} */ [room]) => room).join(', ');
			let reason = `Autolocked for having punishments in ${punishments.length} rooms: ${rooms}`;
			// @ts-ignore
			let message = `${user.name || userid} was locked for having punishments in ${punishments.length} rooms: ${punishmentText}`;

			Punishments.autolock(user, 'staff', 'PunishmentMonitor', reason, message);
			if (typeof user !== 'string') user.popup("|modal|You've been locked for breaking the rules in multiple chatrooms.\n\nIf you feel that your lock was unjustified, you can still PM staff members (%, @, &, and ~) to discuss it" + (Config.appealurl ? " or you can appeal:\n" + Config.appealurl : ".") + "\n\nYour lock will expire in a few days.");
		} else {
			// @ts-ignore
			Monitor.log(`[PunishmentMonitor] ${user.name || userid} currently has punishments in ${punishments.length} rooms: ${punishmentText}`);
		}
	}
};

redpajama_set_name: RedPajamaGithub


text:
Q: Pundit Scope Involving Belongs_To Association I have a Pundit policy for the Entity model and I'm trying to implement a scope. I have the following models:
Entity
  belongs_to :project

Project
  has_many :entities
  has_many :assignments
  has_many :account_users, through: :assignments, dependent: :destroy

Assignment
  belongs_to :account_user
  belongs_to :project

Project has a field, access_type, that can be: Unlimited, Partial, Admin_Only
When a project's access_type is Partial, an admin can assign users to the project, allowing them access (via the assignments join table).
How can I write a Pundit Scope to allow a non-admin user to see only those entities where at least one of the following criteria are met:

*

*The entity is not assigned to a project (project.nil?)

*The entity is assigned to a project with access_type: unlimited (this is an enum, so project.unlimited_access? also works)

*The entity is assigned to a project with access_type: partial AND the user has been given access to the entity's project via an assignment
Here is the policy as it currently stands (the other methods are working correctly, I'm just having trouble creating the scope):
class EntityPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if @account_user.user.admin?
        # If overall app admin then allow access
        scope.all
      elsif @account_user.admin?
        # If the user is an admin in this account then allow access
        scope.all
      else
        # If project is nil OR does not limit access OR (it allows partial access AND this user is permissioned for this project) then allow access
        # scope.includes(:project).where(project: nil).or(scope.where(project: { access_type: "unlimited" })).or(scope.where(project: {access_type: "partial", id: @account_user.project_ids}))
      end
    end
  end

  attr_reader :user, :project

  def initialize(user, entity)
    # the "user" being passed in is actually a UserContext, see UserContext in app/models
    @account_user = user.account.account_users.find_by(user: user.user)
    @entity = entity
    @project = entity.project
  end

  def index?
    true
  end

    def show?
        if @account_user.user.admin?
            # If overall app admin then allow access
            true
        elsif @project.nil?
            # If the entity is not assigned to a project then allow access
            true
        elsif @project.unlimited_access?
            # If project does not limit access then allow access
            true
        elsif @account_user.admin?
            # If the user is an admin in this account then allow access
            true
        elsif @project.partial_access? && @account_user.projects.include?(project)
            true
        else
            false
        end
    end

    def new?
        true
    end

    def create?
        new?
    end

    def edit?
        if @account_user.user.admin?
            # If overall app admin then allow access
            true
        elsif @project.nil?
            # If the entity is not assigned to a project then allow access
            true
        elsif @project.unlimited_access?
            # If project does not limit access then allow access
            true
        elsif @account_user.admin?
            # If the user is an admin in this account then allow access
            true
        elsif @project.partial_access? && @account_user.projects.include?(project)
            true
        else
            false
        end
    end

    def update?
        edit?
    end

    def destroy?
        if @account_user.user.admin?
            # If overall app admin then allow access
            true
        elsif @project.nil?
            # If the entity is not assigned to a project then allow access
            true
        elsif @account_user.admin?
            # If the user is an admin in this account then allow access
            true
        else
            false
        end
    end
end

class UserContext
  attr_reader :user, :account

  def initialize(user, account)
    @user = user
    @account   = account
  end
end

Edit:
Here's the scope in my ProjectPolicy, which is working as far as determining what projects a user can see. The Entity scope needs to do basically the same thing but for the project's entities instead of for the project itself.
class ProjectPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if @account_user.user.admin?
        # If overall app admin then allow access
        scope.all
      elsif @account_user.admin?
        # If the user is an admin in this account then allow access
        scope.all
      else
        # If project does not limit access or it allows partial access AND this user is permissioned for this project then allow access
        scope.where(access_type: :unlimited).or(scope.where(access_type: :partial, id: @account_user.project_ids))
      end
    end
  end

  attr_reader :user, :project

  def initialize(user, project)
    # the "user" being passed in is actually a UserContext, see UserContext in app/models
    @account_user = user.account.account_users.find_by(user: user.user)
    @project = project
  end

Edit 2:
I got pretty close with this, however, it is not returning entities that have project_id of nil because the joins is eliminating any entities without a project assigned:
scope.joins(:project).where(project: nil)
.or(scope.joins(:project).where(projects: { access_type: :unlimited }))
.or(scope.joins(:project).where(projects: { access_type: :partial, id: @account_user.project_ids }))

So I then went with this, which seems to be working but feels less clean. Is there any way to combine this into a single query instead of combining the results of two? Is it bad to do it this way for now?
project_is_nil_ids = scope.where(project_id: nil).pluck(:id)
access_is_allowed_ids = scope.joins(:project).where(projects: { access_type: :unlimited }).or(scope.joins(:project).where(projects: { access_type: :partial, id: @account_user.project_ids })).pluck(:id)
scope.where(id: project_is_nil_ids + access_is_allowed_ids)

A: I have the feeling that you need to implement ProjectPolicy instead of EntityPolicy (may be I am wrong). I wrote it according to my understanding.
class ProjectPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if @user.admin? || @account_user.admin?
        scope.all
      else
        scope.visible_by_user(@user.id)

        # in project.rb
        # scope :visible_by_user, ->(user_id) {
        #   left_joins(:entities, :account_users).where(
        #     """
        #       projects.access_type = 'unlimited' OR
        #       entities.id IS NULL OR
        #       (projects.access_type = 'partial' AND account_users.user_id = :user_id)
        #     """, user_id: user_id
        #   )
        # }
      end
    end
  end

  attr_reader :user, :project

  def initialize(user_context, project)
    @user = user_context.user
    @account = user_context.account
    @account_user = @account.account_users.find_by(user: user)
    @record = project
  end
end


The holy month of Ramadan is right around the corner. Muslims across the world and from all walks of life will be uniting in a fast during the daylight hours, as well as engaging in worship and focusing on self-reflection and family. One of the Five Pillars of Islam, Zakat, is the practice of charitable giving and so Ramadan is also a time of increased charity especially in Muslim countries. During Ramadan, both fasting and acts of charity are obligatory upon Muslims who are able to do so. Fasting from sunrise to sunset is representative of a spiritual cleansing, an effort to become closer to God, and an attempt to gain a better understanding of human suffering. The act of fasting calls for Muslims to practice self-discipline and sacrifice, as well as reflect upon and show compassion for the poor and less fortunate. Muslims are reminded to be generous and increase their charitable activities. As a result, during Ramadan, much charitable giving is done by the community. Donations are usually focused on giving to those stricken by poverty and hunger. e-Mal powered by eFatoora is focussing on a long-term donation structure for 5 selected charities within the UAE that will in return offer more than food.
There is little doubt that the number of charities has increased, placing a greater need for the generosity of the general public. And with the widespread use of smartphones, there has been an ease to the process of giving, as people use their devices to research, shop, but more importantly donate easily. e-Mal plans to implement giving back to society as part of the happiness vision by supporting 5 worthy causes within the UAE and closing the gap between society. According to a report in Forbes in 2013, the event of such online services as PayPal had led to an increase in the number of charitable donations being given. e-Mal plans to launch an innovative way of supporting local charities and making it fun for the donator also by allowing them to be challenged and earn for their chosen charity, whilst at the same time liaising with several other users, creating new friendships and happiness. In the UAE there is a particular drive by the community to help those less well off, including laborers – each year hundreds donate food, bedding, toiletries and clothes which are then handed out to workers as care packages. e-Mal plans to promote this generosity by offering the same back to society. For the Emirati community, Ramadan is a lot more than just a month of fasting and praying. It is a season of reflection and reconnection that transcends religious beliefs and e-Mal has chosen the upcoming prosperous month to launch this long-term donation program not only during Ramadan but throughout the whole year.
Giving back to society should happen all year 'round, not just during Ramadan.


American Pride Women's Sneakers. Take your shoe art to another level and make a statement with our awesome designs.
All of our Women's Sneakers are designed, custom-made-to-order and handcrafted to the highest quality standards. Our exclusive designs are not sold in stores.
Walk, work or play in comfort.
Fantastic gift for a friend, family member or treat yourself.
Many exclusive designs to choose from.


A bunch of photos, results and a description of the thrilling Huckin' Eh mountain bike race.
We've been praying for rain all summer but maybe it could have held off just one more day. It wasn't exactly a downpour but that pitter patter of precipitation last Sunday turned the Huckin' Eh' downhill bike race into a different event.
The aftermath was quite different as well, especially the cleanup of clothing and equipment, which as well-covered with mud.
There were two races at the event held at the Ptarmigan Trail. First was the regular downhill race, somewhere between four to ten minutes of an exhilarating ride down what seemed like a straight drop through trees rocks and a tiny trail.
If that was not exciting enough, for the next race, chains were removed from the bikes and riders had to navigate without the use of pedals that would ordinarily drive them.
A shuttle took bikes and riders back up the hill following the runs.
Safety was of key importance. Riders left at one minute intervals so there was little passing on the narrow trail. Of course there were a few minor crashes on the way down, but there were no injuries. Riders were well covered with helmets, gloves and a variety of other protective equipment.
Halfway down the course here was a first aid station with experienced personnel.
One particular problem was the mud flying up into the riders faces. Most had goggles that would fit over the required helmets but visibility was soon almost nothing and there would be no wiping until the end of the race.
Using no goggles was not much better as tiny bits would go into the eyes. Fun stuff.
Heiko Krause had the fastest times overall and won both races in the 15-18 category. The experienced youngster simply flew down the hill.
Saturday's event was a bit different as the bikers had to go up the hill on their bikes prior to riding back down again. This was definitely an event which challenged the riders' fitness as winners took almost one hour to complete the course. Others took as long as 90 minutes.
Youngsters in the race did a revised course that took a similar amount of time. Fastest was Milo MacDougal at 49:13 in the 11-12 class.
Not to be deterred by the courses' difficulty was Isabella Coupe who took 1:41.55, in winning the girls under 9 group.
Hayden Graham navigates through the a less muddy portion of the Huckin' Eh downhill bike race. The race was sponsored by the McBike sports shop.


Residents of one Australian town are furious over a nine-foot-long cat sculpture set to be put on display at their local community center. The problem? It's not a case of feline discrimination, rather that "Bronze Cat" by artist Dean Bowen, cost the town a reported sixty thousand dollars.
One opposing council member proclaimed, "Whilst I have great respect for artists and their work, I cannot in good conscience and in decent taste support paying public money on something which in my opinion, its appearance resembles a warthog and not a cat." (Ed note: So if it looked more like a cat, she'd be ok with it?). Other protestors have also called the pricey sculpture "disgusting" and not worth the money.
So what say you, fine readers? Is this controversial cat-hog worth the four paws it stands on?
"It's a talking point, a giant cat, and I think that's a great thing about art that people will talk about it. Some people will think it's cute, some people will well might think it's huge. I think it's a beautiful representation of home and art."
Nine feet of bronze ain't cheap! I LOVE the cat and some people apparently need glasses. Warthog. As if.
AHAAHAHHAHAA I WOULD HAVE GLADLY PUT MONEY TOWARDS THAT THAN THE UGLY ART THAT WAS INSTALLED ON OUR FREEWAY... A GIANT SHINY ORB! REALLY!?!?!? AND WE ARE SUPPOSED TO DRIVE WITH THE SUN BEAMED INTO OUR EYEBALLS HOW? OR THE CHIPPED TEACUP ON ITS SIDE? SOMEONE HERE LOVED IT SO MUCH, THEY SPRAYPAINTED A TOILET GOLD AND PUT IT UP...THE PEOPLE WHO CONTROL SAID "ART" DIDN'T EVEN NOTICE UNTIL 2 WEEKS LATER WHEN NEWS COVERAGE HAD STARTED.
Hmm I'm not keen, but then, I'm not overly appreciative of modern art in general. Definitely not worth $60,000!
I think whether or not one likes the sculpture and whether or not a community should, in good conscience, spend $60,000 on an art installation are two different issues. I wouldn't care if it was the Mona Lisa. Communities world-wide are struggling to provide, basic, necessary services to their citizens. Spending $60,000 on an art installation is unconscionable.
I think the very least the Aussi's got for the sixty smackers, was to put their town on the map! As art goes, it's a steal. I love it and wish we had a companion piece here in San Diego the land of public atrocious art. I can visualize it even being larger, say a cross walk over one of our roadways.
If people can sit on it and take photos of themselves, then they will enjoy it. I think it's pretty cute.


Turn on the content that you want to automatically download. After you turn on Automatic Downloads for music, apps, or books and audiobooks on all of your devices, all new purchases that you make in the App Store, iTunes Store, or Apple Books on any device download automatically to all of your devices.... QUESTIONS ABOUT DOWNLOADING VIDEOS FROM WEB PLAYERS IDM starts downloading video automatically when I start playing it in a browser. How to avoid this?
7/04/2017 · Here is how to auto generate subtitles from any video with Google docs. It also works if you want to convert audio to text. Useful for creating subtitles and closed Captions for all your Youtube... Download: Adjust how many videos can be downloaded at the same time and automatically resume unfinished tasks upon startup. Location : Define a path to save downloaded YouTube videos. Why choose wondershare?
QUESTIONS ABOUT DOWNLOADING VIDEOS FROM WEB PLAYERS IDM starts downloading video automatically when I start playing it in a browser. How to avoid this?... Download automatically detected media-> If the checkbox is ticked, then VSO Downloader will automatically download multimedia it detects in the browser. Auto rename file -> The files you download will generally have generic names, like "playvideo.flv" .
For details on these options, such as starting the video automatically or playing the video full-screen, see Set the 'Playback' options for a video in your presentation. Preview the video Once the video rectangle is on your slide, select it, then select the Playback tab on the PowerPoint ribbon.
On the picture below we selected the adding option, so all new tracks will be automatically added to the download queue upon next check. You can also select save as audio option to save only audio stream, it's useful for podcasts, audio books and of course music.
Lately when I open a webpage with a video in Google Chrome, the video starts downloading automatically through Internet Download Manager. This does not happen on YouTube, however.